Skip to content

Commit 5f82029

Browse files
ugur-vaadinvursen
andauthored
feat: implement scrolltoitem feature for treegrid (#8070)
* feat: add scrolltoitem for treedataproviders * test: remove expand param * test: refactor test page to separate test data * test: move static fields * test: expose test data methods * refactor: scroll to item logic cleanup * chore: add todo to use the correct data source for nested scroll * refactor: use correct data source for nested scroll * refactor: extract logic that will reside in data providers * refactor: cleanup * Revert "refactor: cleanup" This reverts commit c0a5eff. * Revert "refactor: extract logic that will reside in data providers" This reverts commit 57483aa. * refactor: extract data provider logic into methods with same signatures * docs: update javadoc to reflect the changes * refactor: cleanup * refactor: cleanup and simplify * Revert "refactor: cleanup and simplify" This reverts commit 3f40845. * test: add unit tests for scrolltoitem * chore: update todo description * chore: remove demo page * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/treegrid/TreeGridScrollTest.java Co-authored-by: Sergey Vinogradov <[email protected]> * chore: run formatter * refactor: use data provider api and update docs * test: add new providers * fix: cast data provider * chore: remove todo * docs: add p to javadoc for better formatting * test: update unit tests for custom data providers * test: fix typo in test name * refactor: move logic to data communicator do not determine item for nested * test: add tests for items that do not belong to the tree * refactor: get data provider once * refactor: extract communicator and expand on scroll to item * chore: fix compilation * docs: add clarifications to new methods in tree grid data communicator * refactor: add resolve item method to communicator * refactor: do not resolve index path * refactor: separate resolve item method * Revert "refactor: separate resolve item method" This reverts commit 3425eab. * refactor: expand after index path is resolved * fix: expand before getting index path * refactor: throw exception in communicator and return int array * docs: mention expand event is not fired on scroll * docs: add helper table for which methods should be implemented * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/treegrid/TreeGrid.java Co-authored-by: Sergey Vinogradov <[email protected]> * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/treegrid/TreeGrid.java Co-authored-by: Sergey Vinogradov <[email protected]> * test: remove lazy provider tests and cleanup * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/treegrid/TreeGrid.java Co-authored-by: Sergey Vinogradov <[email protected]> * refactor: use linked list to simplify implementation * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/treegrid/ScrollToItemTest.java Co-authored-by: Sergey Vinogradov <[email protected]> * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/treegrid/ScrollToItemTest.java Co-authored-by: Sergey Vinogradov <[email protected]> * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/test/java/com/vaadin/flow/component/treegrid/ScrollToItemTest.java Co-authored-by: Sergey Vinogradov <[email protected]> * test: cleanup * test: add it for scrolling to item * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/treegrid/TreeGrid.java Co-authored-by: Sergey Vinogradov <[email protected]> * Update vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/treegrid/TreeGrid.java Co-authored-by: Sergey Vinogradov <[email protected]> * docs: update table with links * docs: remove link --------- Co-authored-by: Sergey Vinogradov <[email protected]>
1 parent c0e1db9 commit 5f82029

File tree

5 files changed

+484
-51
lines changed

5 files changed

+484
-51
lines changed

vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/main/java/com/vaadin/flow/component/treegrid/it/TreeGridScrollToPage.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ public TreeGridScrollToPage() {
6464
});
6565
scrollToIndex.setId("scroll-to-index");
6666

67-
add(grid, expandAll, scrollToStart, scrollToEnd, scrollToIndex);
67+
Input scrollToItem = new Input(ValueChangeMode.ON_BLUR);
68+
scrollToItem.setPlaceholder("Scroll to item (by name)");
69+
scrollToItem.setWidth("200px");
70+
scrollToItem.addValueChangeListener(
71+
event -> grid.scrollToItem(event.getValue()));
72+
scrollToItem.setId("scroll-to-item");
73+
74+
add(grid, expandAll, scrollToStart, scrollToEnd, scrollToIndex,
75+
scrollToItem);
6876
}
6977
}

vaadin-grid-flow-parent/vaadin-grid-flow-integration-tests/src/test/java/com/vaadin/flow/component/treegrid/it/TreeGridScrollToIT.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,17 @@ public class TreeGridScrollToIT extends AbstractComponentIT {
4242

4343
private TestBenchElement scrollToIndexInput;
4444

45+
private TestBenchElement scrollToItemInput;
46+
4547
@Before
4648
public void init() {
4749
open();
48-
grid = $(TreeGridElement.class).first();
50+
grid = $(TreeGridElement.class).waitForFirst();
4951
expandAllButton = $("button").id("expand-all");
5052
scrollToStartButton = $("button").id("scroll-to-start");
5153
scrollToEndButton = $("button").id("scroll-to-end");
5254
scrollToIndexInput = $("input").id("scroll-to-index");
55+
scrollToItemInput = $("input").id("scroll-to-item");
5356
}
5457

5558
@Test
@@ -101,6 +104,13 @@ public void expandAll_scrollToIndex30_1_correctFirstVisibleItem() {
101104
assertFirstVisibleRowContent("Dad 30/1");
102105
}
103106

107+
@Test
108+
public void expandAll_scrollToItem30_1_correctFirstVisibleItem() {
109+
expandAllButton.click();
110+
scrollToItemInput.sendKeys("Dad 30/1", Keys.TAB);
111+
assertFirstVisibleRowContent("Dad 30/1");
112+
}
113+
104114
@Test
105115
public void scrollToIndex30_1_correctFirstVisibleItem() {
106116
scrollToIndexInput.sendKeys("30-1", Keys.TAB);

vaadin-grid-flow-parent/vaadin-grid-flow/src/main/java/com/vaadin/flow/component/treegrid/TreeGrid.java

Lines changed: 59 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,12 @@
3030
import com.vaadin.flow.component.ComponentUtil;
3131
import com.vaadin.flow.component.dependency.JsModule;
3232
import com.vaadin.flow.component.grid.Grid;
33-
import com.vaadin.flow.component.grid.Grid.Column;
3433
import com.vaadin.flow.component.grid.GridArrayUpdater;
3534
import com.vaadin.flow.component.grid.dataview.GridDataView;
3635
import com.vaadin.flow.component.grid.dataview.GridLazyDataView;
3736
import com.vaadin.flow.component.grid.dataview.GridListDataView;
3837
import com.vaadin.flow.component.internal.AllowInert;
3938
import com.vaadin.flow.data.binder.PropertyDefinition;
40-
import com.vaadin.flow.data.provider.ArrayUpdater;
4139
import com.vaadin.flow.data.provider.BackEndDataProvider;
4240
import com.vaadin.flow.data.provider.CallbackDataProvider;
4341
import com.vaadin.flow.data.provider.CompositeDataGenerator;
@@ -50,6 +48,7 @@
5048
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider.HierarchyFormat;
5149
import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery;
5250
import com.vaadin.flow.data.provider.hierarchy.TreeData;
51+
import com.vaadin.flow.data.provider.hierarchy.TreeDataProvider;
5352
import com.vaadin.flow.data.renderer.ComponentRenderer;
5453
import com.vaadin.flow.data.renderer.LitRenderer;
5554
import com.vaadin.flow.data.renderer.Renderer;
@@ -214,43 +213,6 @@ public TreeGrid(HierarchicalDataProvider<T, ?> dataProvider) {
214213
setDataProvider(dataProvider);
215214
}
216215

217-
private static class TreeGridDataCommunicator<T>
218-
extends HierarchicalDataCommunicator<T> {
219-
private Element element;
220-
221-
public TreeGridDataCommunicator(Element element,
222-
CompositeDataGenerator<T> dataGenerator,
223-
ArrayUpdater arrayUpdater,
224-
SerializableSupplier<ValueProvider<T, String>> uniqueKeyProviderSupplier) {
225-
super(dataGenerator, arrayUpdater, element.getNode(),
226-
uniqueKeyProviderSupplier);
227-
this.element = element;
228-
}
229-
230-
@Override
231-
public void reset() {
232-
super.reset();
233-
if (element != null) {
234-
element.callJsFunction("$connector.reset");
235-
}
236-
}
237-
238-
@Override
239-
protected List<T> preloadFlatRangeForward(int start, int length) {
240-
return super.preloadFlatRangeForward(start, length);
241-
}
242-
243-
@Override
244-
protected List<T> preloadFlatRangeBackward(int start, int length) {
245-
return super.preloadFlatRangeBackward(start, length);
246-
}
247-
248-
@Override
249-
protected int resolveIndexPath(int... path) {
250-
return super.resolveIndexPath(path);
251-
}
252-
}
253-
254216
private static class TreeDataCommunicatorBuilder<T>
255217
extends DataCommunicatorBuilder<T, GridArrayUpdater> {
256218

@@ -1102,7 +1064,7 @@ int setViewportRangeByIndexPath(int[] path, int padding) {
11021064
// page-aligned.
11031065
dataCommunicator.preloadFlatRangeForward(flatIndex, padding + pageSize);
11041066

1105-
// Repeat the process backward to preload enough items behing the
1067+
// Repeat the process backward to preload enough items behind the
11061068
// resolved flat index. Adding the page size is essential. Without it,
11071069
// the following call to dataCommunicator.setViewportRange will try to
11081070
// load uncovered expanded items forward, shifting the range and causing
@@ -1130,21 +1092,69 @@ int setViewportRangeByIndexPath(int[] path, int padding) {
11301092
}
11311093

11321094
/**
1133-
* TreeGrid does not support scrolling to a given item. Use
1134-
* {@link #scrollToIndex(int...)} instead.
1095+
* Scrolls to an item within the tree. If the ancestors of the item are not
1096+
* expanded, this method expands them before scrolling. Does not fire any
1097+
* {@link ExpandEvent}s for the ancestors expanded during scrolling.
11351098
* <p>
1136-
* This method is inherited from Grid and has been marked as deprecated to
1137-
* indicate that it is not supported. This method will throw an
1138-
* {@link UnsupportedOperationException}.
1099+
* In order to be able to use this method, the data provider should
1100+
* implement {@link HierarchicalDataProvider#getParent(T)} and
1101+
* {@link HierarchicalDataProvider#getItemIndex(T, HierarchicalQuery)}. The
1102+
* following table shows which methods have to be explicitly implemented
1103+
* based on the data provider types.
1104+
* <table>
1105+
* <tr>
1106+
* <th>HierarchicalDataProvider</th>
1107+
* <th>{@link HierarchicalDataProvider#isInMemory() isInMemory()}</th>
1108+
* <th>{@link HierarchicalDataProvider#getItemIndex(T, HierarchicalQuery)
1109+
* getItemIndex()}</th>
1110+
* <th>{@link HierarchicalDataProvider#getParent(T) getParent()}</th>
1111+
* </tr>
1112+
* <tr>
1113+
* <td>{@link HierarchyFormat#NESTED HierarchyFormat.NESTED}</td>
1114+
* <td>true</td>
1115+
* <td>not required</td>
1116+
* <td>required</td>
1117+
* </tr>
1118+
* <tr>
1119+
* <td>{@link HierarchyFormat#NESTED HierarchyFormat.NESTED}</td>
1120+
* <td>false</td>
1121+
* <td>required</td>
1122+
* <td>required</td>
1123+
* </tr>
1124+
* <tr>
1125+
* <td>{@link HierarchyFormat#FLATTENED HierarchyFormat.FLATTENED}</td>
1126+
* <td>true</td>
1127+
* <td>not required</td>
1128+
* <td>required</td>
1129+
* </tr>
1130+
* <tr>
1131+
* <td>{@link HierarchyFormat#FLATTENED HierarchyFormat.FLATTENED}</td>
1132+
* <td>false</td>
1133+
* <td>required</td>
1134+
* <td>required</td>
1135+
* </tr>
1136+
* <tr>
1137+
* <td>{@link TreeDataProvider}</td>
1138+
* <td>true</td>
1139+
* <td>not required</td>
1140+
* <td>not required</td>
1141+
* </tr>
1142+
* </table>
11391143
*
11401144
* @param item
11411145
* the item to scroll to
1142-
* @deprecated
1146+
* @throws IllegalArgumentException
1147+
* if the item does not belong to the tree
11431148
*/
1144-
@Deprecated
11451149
@Override
11461150
public void scrollToItem(T item) {
1147-
throw new UnsupportedOperationException(
1148-
"scrollToItem method is not supported in TreeGrid");
1151+
Objects.requireNonNull(item, "Item to scroll to cannot be null.");
1152+
var indexPath = ((TreeGridDataCommunicator<T>) getDataCommunicator())
1153+
.resolveItem(item);
1154+
if (indexPath.length == 1) {
1155+
scrollToIndex(indexPath[0]);
1156+
} else {
1157+
scrollToIndex(indexPath);
1158+
}
11491159
}
11501160
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/*
2+
* Copyright 2000-2025 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.treegrid;
17+
18+
import java.util.ArrayList;
19+
import java.util.LinkedList;
20+
import java.util.List;
21+
22+
import com.vaadin.flow.data.provider.ArrayUpdater;
23+
import com.vaadin.flow.data.provider.CompositeDataGenerator;
24+
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataCommunicator;
25+
import com.vaadin.flow.data.provider.hierarchy.HierarchicalDataProvider;
26+
import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery;
27+
import com.vaadin.flow.data.provider.hierarchy.TreeDataProvider;
28+
import com.vaadin.flow.dom.Element;
29+
import com.vaadin.flow.function.SerializableSupplier;
30+
import com.vaadin.flow.function.ValueProvider;
31+
32+
/**
33+
* This class is for internal use only.
34+
*/
35+
class TreeGridDataCommunicator<T> extends HierarchicalDataCommunicator<T> {
36+
private final Element element;
37+
38+
public TreeGridDataCommunicator(Element element,
39+
CompositeDataGenerator<T> dataGenerator, ArrayUpdater arrayUpdater,
40+
SerializableSupplier<ValueProvider<T, String>> uniqueKeyProviderSupplier) {
41+
super(dataGenerator, arrayUpdater, element.getNode(),
42+
uniqueKeyProviderSupplier);
43+
this.element = element;
44+
}
45+
46+
@Override
47+
public void reset() {
48+
super.reset();
49+
if (element != null) {
50+
element.callJsFunction("$connector.reset");
51+
}
52+
}
53+
54+
@Override
55+
public List<T> preloadFlatRangeForward(int start, int length) {
56+
return super.preloadFlatRangeForward(start, length);
57+
}
58+
59+
@Override
60+
public List<T> preloadFlatRangeBackward(int start, int length) {
61+
return super.preloadFlatRangeBackward(start, length);
62+
}
63+
64+
@Override
65+
public int resolveIndexPath(int... path) {
66+
return super.resolveIndexPath(path);
67+
}
68+
69+
/**
70+
* Expands all ancestors of the item and returns the index path of it.
71+
* Returns empty list if item is not found.
72+
*
73+
* @param item
74+
* the item to resolve
75+
* @return the index path of the item
76+
*/
77+
public int[] resolveItem(T item) {
78+
var ancestors = getAncestors(item);
79+
expand(ancestors);
80+
return getIndexPath(item, ancestors);
81+
}
82+
83+
/**
84+
* Gets the index path for the given item. Returns empty list if item is not
85+
* found.
86+
* <p>
87+
* Accepts the list of ancestors for optimization purposes where the list
88+
* already exists.
89+
* <p>
90+
* In order to be able to use this method, the data provider should
91+
* implement
92+
* {@link HierarchicalDataProvider#getItemIndex(T, HierarchicalQuery)}. Any
93+
* in-memory data provider implements it by default.
94+
*
95+
* @param item
96+
* the item to get the index path for
97+
* @param ancestors
98+
* the ordered list of the ancestors of the item
99+
* @return index path for the given item
100+
*/
101+
private int[] getIndexPath(T item, List<T> ancestors) {
102+
var path = new ArrayList<Integer>();
103+
if (getDataProvider().getHierarchyFormat()
104+
.equals(HierarchicalDataProvider.HierarchyFormat.NESTED)) {
105+
path.addAll(getAncestorPath(ancestors));
106+
}
107+
var itemIndex = getItemIndex(item,
108+
path.isEmpty() ? null : ancestors.get(ancestors.size() - 1));
109+
if (itemIndex == -1) {
110+
throw new IllegalArgumentException("Item does not exist.");
111+
}
112+
path.add(itemIndex);
113+
return path.stream().mapToInt(i -> i).toArray();
114+
}
115+
116+
/**
117+
* Gets the ordered list of ancestors for the given item.
118+
* <p>
119+
* In order to be able to use this method, the data provider should
120+
* implement {@link HierarchicalDataProvider#getParent(T)}.
121+
* {@link TreeDataProvider} implements it by default.
122+
*
123+
* @param item
124+
* the item to get the ancestors for
125+
* @return ordered list of ancestors of the given item
126+
*/
127+
private List<T> getAncestors(T item) {
128+
var ancestors = new LinkedList<T>();
129+
while ((item = getDataProvider().getParent(item)) != null) {
130+
ancestors.addFirst(item);
131+
}
132+
return ancestors;
133+
}
134+
135+
private List<Integer> getAncestorPath(List<T> ancestors) {
136+
var ancestorPath = new ArrayList<Integer>();
137+
for (var i = 0; i < ancestors.size(); i++) {
138+
var ancestorIndex = getItemIndex(ancestors.get(i),
139+
i == 0 ? null : ancestors.get(i - 1));
140+
if (ancestorIndex == -1) {
141+
throw new IllegalArgumentException("Item does not exist.");
142+
}
143+
ancestorPath.add(ancestorIndex);
144+
}
145+
return ancestorPath;
146+
}
147+
148+
private int getItemIndex(T item, T parent) {
149+
var query = buildQuery(parent, 0, Integer.MAX_VALUE);
150+
return ((HierarchicalDataProvider<T, Object>) getDataProvider())
151+
.getItemIndex(item, query);
152+
}
153+
}

0 commit comments

Comments
 (0)