TreeGridScrollToIndex.java
package com.vaadin.demo.component.treegrid;
import com.vaadin.demo.domain.DataService;
import com.vaadin.demo.domain.Person;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.FlexComponent.Alignment;
import com.vaadin.flow.component.textfield.IntegerField;
import com.vaadin.flow.component.treegrid.TreeGrid;
import com.vaadin.flow.data.provider.hierarchy.AbstractHierarchicalDataProvider;
import com.vaadin.flow.data.provider.hierarchy.HierarchicalQuery;
import com.vaadin.flow.router.Route;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
@Route("tree-grid-scroll-to-index")
public class TreeGridScrollToIndex extends Div {
    private IntegerField parentIndexField = new IntegerField("Parent index");
    private IntegerField childIndexField = new IntegerField("Child index");
    private Button scrollToIndexButton = new Button();
    private Map<Person, List<Integer>> personToIndexAddress = new HashMap<>();
    private TreeGrid<Person> treeGrid = new TreeGrid<>();
    public TreeGridScrollToIndex() {
        treeGrid.setDataProvider(new LazyLoadingProvider());
        treeGrid.setUniqueKeyDataGenerator("key", (person) -> {
            return String.valueOf(person.getId());
        });
        treeGrid.expand(DataService.getManagers());
        treeGrid.addHierarchyColumn(Person::getFirstName).setWidth("200px")
                .setFlexGrow(0).setHeader("First name");
        treeGrid.addSelectionListener(e -> {
            if (e.getFirstSelectedItem().isPresent()) {
                Person person = e.getFirstSelectedItem().get();
                List<Integer> indexAddress = personToIndexAddress.get(person);
                if (indexAddress != null) {
                    parentIndexField.setValue(indexAddress.get(0));
                    if (indexAddress.size() > 1) {
                        childIndexField.setValue(indexAddress.get(1));
                    }
                }
            }
        });
        treeGrid.addColumn(person -> StringUtils
                .join(personToIndexAddress.get(person), ", ")).setWidth("80px")
                .setFlexGrow(0).setHeader("Index");
        treeGrid.addColumn(Person::getEmail).setHeader("Email");
        add(treeGrid);
        HorizontalLayout controls = new HorizontalLayout();
        controls.setSpacing(true);
        controls.setAlignItems(Alignment.END);
        parentIndexField.setWidth("120px");
        childIndexField.setWidth("120px");
        parentIndexField.setMin(0);
        childIndexField.setMin(0);
        parentIndexField.setStepButtonsVisible(true);
        childIndexField.setStepButtonsVisible(true);
        parentIndexField.setValue(13);
        childIndexField.setValue(6);
        parentIndexField.addValueChangeListener(e -> updateSelectedItem());
        childIndexField.addValueChangeListener(e -> updateSelectedItem());
        controls.add(parentIndexField);
        controls.add(childIndexField);
        scrollToIndexButton.addClickListener(e -> {
            int[] indexesToScrollTo = { parentIndexField.getValue(),
                    childIndexField.getValue() };
            // tag::snippet[]
            treeGrid.scrollToIndex(indexesToScrollTo);
            // end::snippet[]
        });
        controls.add(scrollToIndexButton);
        add(controls);
    }
    private void updateSelectedItem() {
        treeGrid.select(null);
        Integer parentIndex = parentIndexField.getValue();
        Integer childIndex = childIndexField.getValue();
        personToIndexAddress.entrySet().stream().filter(entry -> {
            List<Integer> indexes = entry.getValue();
            return indexes.size() == 2
                    && List.of(parentIndex, childIndex).equals(indexes);
        }).findFirst().ifPresent(entry -> {
            treeGrid.select(entry.getKey());
        });
        scrollToIndexButton
                .setText("Scroll to index: " + parentIndex + ", " + childIndex);
    }
    private class LazyLoadingProvider
            extends AbstractHierarchicalDataProvider<Person, Void> {
        @Override
        public int getChildCount(HierarchicalQuery<Person, Void> query) {
            return (int) this.fetchChildren(query).count();
        }
        @Override
        public Stream<Person> fetchChildren(
                HierarchicalQuery<Person, Void> query) {
            List<Person> people;
            if (query.getParent() == null) {
                people = DataService.getManagers();
            } else {
                people = DataService.getPeople(query.getParent().getId());
            }
            int limit = query.getLimit();
            int offset = query.getOffset();
            // Cache the index address of each person for demo purposes
            AtomicInteger personIndex = new AtomicInteger(0);
            people.stream().skip(offset).limit(limit).forEach(person -> {
                int index = offset + personIndex.getAndIncrement();
                List<Integer> parentIndexAddress = personToIndexAddress
                        .get(query.getParent());
                List<Integer> indexAddress = parentIndexAddress == null
                        ? List.of(index)
                        : List.of(parentIndexAddress.get(0), index);
                personToIndexAddress.put(person, indexAddress);
            });
            updateSelectedItem();
            return people.stream().skip(offset).limit(limit);
        }
        @Override
        public boolean hasChildren(Person item) {
            return DataService.getPeople(item.getId()).size() > 0;
        }
        @Override
        public boolean isInMemory() {
            return false;
        }
    }
}
tree-grid-scroll-to-index.tsx
import React, { useMemo, useRef } from 'react';
import { useComputed, useSignal } from '@vaadin/hilla-react-signals';
import { Button } from '@vaadin/react-components/Button.js';
import {
  Grid,
  type GridDataProviderCallback,
  type GridDataProviderParams,
  type GridElement,
} from '@vaadin/react-components/Grid.js';
import { GridColumn } from '@vaadin/react-components/GridColumn.js';
import { GridTreeColumn } from '@vaadin/react-components/GridTreeColumn.js';
import { HorizontalLayout } from '@vaadin/react-components/HorizontalLayout.js';
import { IntegerField } from '@vaadin/react-components/IntegerField.js';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
type PersonOrId =
  | Person
  | {
      id: number;
    };
function Example() {
  const gridRef = useRef<GridElement>(null);
  const idToIndexes = useMemo(() => new Map<number, number[]>(), []);
  const expandedItems = useSignal<Person[]>([]);
  const indexesToScrollTo = useSignal<number[]>([13, 6]);
  const indexesToScrollToRef = useRef<number[]>(indexesToScrollTo.value);
  indexesToScrollToRef.current = indexesToScrollTo.value;
  const dataProvider = useMemo(
    () =>
      async (
        params: GridDataProviderParams<PersonOrId>,
        callback: GridDataProviderCallback<PersonOrId>
      ) => {
        const startIndex = params.page * params.pageSize;
        const { people, hierarchyLevelSize } = await getPeople({
          count: params.pageSize,
          startIndex,
          managerId: params.parentItem ? params.parentItem.id : null,
        });
        // Cache the index address of each person for demo purposes
        people.forEach((person, idx) => {
          const index = startIndex + idx;
          const parentIndexes = params.parentItem
            ? (idToIndexes.get(params.parentItem.id) ?? [])
            : [];
          const indexAddress = [...parentIndexes, index];
          idToIndexes.set(person.id, indexAddress);
          if (
            indexAddress[0] === indexesToScrollToRef.current[0] &&
            indexAddress[1] === indexesToScrollToRef.current[1]
          ) {
            indexesToScrollTo.value = indexAddress;
          }
        });
        if (!expandedItems.value.length && !params.parentItem) {
          // Expand the root level by default
          expandedItems.value = people;
        }
        callback(people, hierarchyLevelSize);
      },
    []
  );
  const selectedItems = useComputed(() => {
    const indexAddress = indexesToScrollTo.value.join(', ');
    const id = Array.from(idToIndexes.entries()).find(
      ([, indexes]) => indexes.join(', ') === indexAddress
    )?.[0];
    return id ? [{ id }] : [];
  });
  return (
    <>
      <Grid
        ref={gridRef}
        itemIdPath="id"
        itemHasChildrenPath="manager"
        dataProvider={dataProvider}
        expandedItems={expandedItems.value}
        selectedItems={selectedItems.value}
        onActiveItemChanged={(e) => {
          if (e.detail.value) {
            indexesToScrollTo.value = idToIndexes.get(e.detail.value.id) ?? [];
          }
        }}
      >
        <GridTreeColumn<Person> path="firstName" width="200px" flexGrow={0} />
        <GridColumn<Person> header="Index" width="80px" flexGrow={0}>
          {({ item }) => idToIndexes.get(item.id)?.join(', ')}
        </GridColumn>
        <GridColumn<Person> path="email" />
      </Grid>
      <HorizontalLayout theme="spacing" className="items-end">
        <IntegerField
          label="Parent index"
          stepButtonsVisible
          min={0}
          style={{ width: '120px' }}
          value={String(indexesToScrollTo.value[0])}
          onChange={(e) => {
            indexesToScrollTo.value = [parseInt(e.target.value) || 0, indexesToScrollTo.value[1]];
          }}
        />
        <IntegerField
          label="Child index"
          stepButtonsVisible
          min={0}
          style={{ width: '120px' }}
          value={String(indexesToScrollTo.value[1])}
          onChange={(e) => {
            indexesToScrollTo.value = [indexesToScrollTo.value[0], parseInt(e.target.value) || 0];
          }}
        />
        <Button
          onClick={() => {
            const grid = gridRef.current;
            if (grid) {
              // tag::snippet[]
              grid.scrollToIndex(...indexesToScrollTo.value);
              // end::snippet[]
            }
          }}
        >
          Scroll to index: {indexesToScrollTo.value.join(', ')}
        </Button>
      </HorizontalLayout>
    </>
  );
}
tree-grid-scroll-to-index.ts
import '@vaadin/button';
import '@vaadin/grid';
import '@vaadin/grid/vaadin-grid-tree-column.js';
import '@vaadin/horizontal-layout';
import '@vaadin/integer-field';
import { html, LitElement } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import type {
  Grid,
  GridActiveItemChangedEvent,
  GridBodyRenderer,
  GridDataProviderCallback,
  GridDataProviderParams,
} from '@vaadin/grid';
import type { IntegerFieldChangeEvent } from '@vaadin/integer-field';
import { getPeople } from 'Frontend/demo/domain/DataService';
import type Person from 'Frontend/generated/com/vaadin/demo/domain/Person';
import { applyTheme } from 'Frontend/generated/theme';
@customElement('tree-grid-scroll-to-index')
export class Example extends LitElement {
  protected override createRenderRoot() {
    const root = super.createRenderRoot();
    // Apply custom theme (only supported if your app uses one)
    applyTheme(root);
    return root;
  }
  @query('vaadin-grid')
  private grid!: Grid<Person>;
  @state()
  private expandedItems?: Person[];
  @state()
  private indexesToScrollTo: number[] = [13, 6];
  @state()
  private idToIndexes = new Map<number, number[]>();
  constructor() {
    super();
    this.dataProvider = this.dataProvider.bind(this);
  }
  async dataProvider(
    params: GridDataProviderParams<Person>,
    callback: GridDataProviderCallback<Person>
  ) {
    const startIndex = params.page * params.pageSize;
    const { people, hierarchyLevelSize } = await getPeople({
      count: params.pageSize,
      startIndex,
      managerId: params.parentItem ? params.parentItem.id : null,
    });
    // Cache the index address of each person for demo purposes
    people.forEach((person, idx) => {
      const index = startIndex + idx;
      const parentIndexes = params.parentItem
        ? (this.idToIndexes.get(params.parentItem.id) ?? [])
        : [];
      const indexes = [...parentIndexes, index];
      this.idToIndexes = new Map(this.idToIndexes).set(person.id, indexes);
    });
    if (!this.expandedItems && !params.parentItem) {
      // Expand the root level by default
      this.expandedItems = people;
    }
    callback(people, hierarchyLevelSize);
  }
  private indexRenderer: GridBodyRenderer<Person> = (root, _, { item }) => {
    root.textContent = this.idToIndexes.get(item.id)?.join(', ') ?? '';
  };
  private getSelectedItems(indexes: number[], idToIndexes: Map<number, number[]>) {
    const id = Array.from(idToIndexes.entries()).find(
      ([, idxs]) => idxs[0] === indexes[0] && idxs[1] === indexes[1]
    )?.[0];
    return id ? [{ id }] : [];
  }
  protected override render() {
    return html`
      <vaadin-grid
        item-id-path="id"
        item-has-children-path="manager"
        .dataProvider="${this.dataProvider}"
        .expandedItems="${this.expandedItems ?? []}"
        .selectedItems="${this.getSelectedItems(this.indexesToScrollTo, this.idToIndexes)}"
        @active-item-changed=${(e: GridActiveItemChangedEvent<Person>) => {
          if (e.detail.value) {
            this.indexesToScrollTo = this.idToIndexes.get(e.detail.value.id) ?? [];
          }
        }}
      >
        <vaadin-grid-tree-column
          path="firstName"
          width="200px"
          flex-grow="0"
        ></vaadin-grid-tree-column>
        <vaadin-grid-column
          header="Index"
          .renderer=${this.indexRenderer}
          width="80px"
          flex-grow="0"
        ></vaadin-grid-column>
        <vaadin-grid-column path="email"></vaadin-grid-column>
      </vaadin-grid>
      <vaadin-horizontal-layout theme="spacing" class="items-end">
        <vaadin-integer-field
          label="Parent index"
          step-buttons-visible
          min="0"
          style="width: 120px"
          .value=${String(this.indexesToScrollTo[0] ?? '')}
          @change=${(e: IntegerFieldChangeEvent) => {
            this.indexesToScrollTo = [parseInt(e.target.value) || 0, this.indexesToScrollTo[1]];
          }}
        ></vaadin-integer-field>
        <vaadin-integer-field
          label="Child index"
          step-buttons-visible
          min="0"
          style="width: 120px"
          .value=${String(this.indexesToScrollTo[1] ?? '')}
          @change=${(e: IntegerFieldChangeEvent) => {
            this.indexesToScrollTo = [this.indexesToScrollTo[0], parseInt(e.target.value) || 0];
          }}
        ></vaadin-integer-field>
        <vaadin-button
          @click=${() => {
            // tag::snippet[]
            this.grid.scrollToIndex(...this.indexesToScrollTo);
            // end::snippet[]
          }}
        >
          Scroll to index: ${this.indexesToScrollTo.join(', ')}
        </vaadin-button>
      </vaadin-horizontal-layout>
    `;
  }
}