You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React DevExtreme DataGrid虚拟滚动下Shift选行焦点丢失问题求助

DevExtreme DataGrid虚拟滚动下Shift+箭头连续选中时焦点丢失的解决方案

问题描述

我在应用中实现了选中行后按Shift+上/下箭头选中连续行的功能,功能正常,但在虚拟滚动(scrolling.mode: virtual)模式下,滚动到DataGrid顶部或底部边缘时焦点会丢失。生产环境中有约8000条记录,必须使用虚拟滚动,请问该如何处理?

问题原因

虚拟滚动模式下,DataGrid仅渲染可视区域内的行,当滚动到边缘时,超出可视区域的行被销毁,导致原代码中通过getCellElement获取的DOM元素不存在,焦点无法正常设置,进而出现丢失问题。

修复方案

1. 启用DataGrid内置的聚焦行功能

开启focusedRowEnabled配置,通过行索引而非DOM元素管理焦点,即使行未渲染,也能在滚动后自动定位到正确行。

2. 替换焦点设置逻辑

将原代码中直接聚焦DOM元素的逻辑,改为设置focusedRowIndex属性,同时添加scrollToRow确保聚焦行滚动到可视区域。

3. 优化键值获取逻辑

虚拟滚动下优先使用DataGrid内置API获取行键,避免依赖未渲染行的DOM信息。

修改后的完整代码

import React, { useRef, useCallback, useEffect } from 'react';
import DataGrid, {
  Scrolling, Paging, Column, HeaderFilter, Search, Selection,
} from 'devextreme-react/data-grid';
import * as AspNetData from 'devextreme-aspnet-data-nojquery';

const dataSource = AspNetData.createStore({
  key: 'Id',
  loadUrl: 'https://js.devexpress.com/Demos/WidgetsGalleryDataService/api/Sales',
});

const App = () => {
  const gridInstanceRef = useRef<any>(null);
  const wrapperRef = useRef<HTMLDivElement>(null);
  const anchorIndexRef = useRef<number | null>(null);
  const activeIndexRef = useRef<number | null>(null);

  const handleInitialized = useCallback((e: any) => {
    gridInstanceRef.current = e.component;
    // 开启聚焦行功能
    gridInstanceRef.current.option('focusedRowEnabled', true);
  }, []);

  const handleRowClick = useCallback((e: any) => {
    const grid = gridInstanceRef.current;
    if (!grid || e.rowType !== 'data') return;

    const rowIndex = e.rowIndex;
    const nativeEvent = e.event;

    if (nativeEvent?.shiftKey && anchorIndexRef.current !== null) {
      const anchor = anchorIndexRef.current;
      const start = Math.min(anchor, rowIndex);
      const end = Math.max(anchor, rowIndex);
      const keys: any[] = [];
      for (let i = start; i <= end; i++) {
        const key = grid.getKeyByRowIndex(i);
        if (key !== undefined) keys.push(key);
      }
      grid.selectRows(keys, false);
      activeIndexRef.current = rowIndex;
      // 同步设置聚焦行索引
      grid.option('focusedRowIndex', rowIndex);
    } else {
      const key = grid.getKeyByRowIndex(rowIndex);
      if (key !== undefined) {
        grid.selectRows([key], false);
      }
      anchorIndexRef.current = rowIndex;
      activeIndexRef.current = rowIndex;
      grid.option('focusedRowIndex', rowIndex);
    }
  }, []);

  useEffect(() => {
    const wrapper = wrapperRef.current;
    if (!wrapper) return;

    const onKeyDown = (event: KeyboardEvent) => {
      if (!event.shiftKey || (event.key !== 'ArrowUp' && event.key !== 'ArrowDown')) return;

      const grid = gridInstanceRef.current;
      if (!grid) return;

      const target = event.target as HTMLElement;
      if (!wrapper.contains(target)) return;
      const tag = target?.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || target?.isContentEditable) return;

      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();

      // 初始化锚点索引
      if (anchorIndexRef.current === null) {
        const selected = grid.getSelectedRowKeys();
        if (selected.length > 0) {
          const idx = grid.getRowIndexByKey(selected[0]);
          if (idx >= 0) {
            anchorIndexRef.current = idx;
            activeIndexRef.current = idx;
          }
        } else {
          const focused = grid.option('focusedRowIndex');
          if (typeof focused === 'number' && focused >= 0) {
            anchorIndexRef.current = focused;
            activeIndexRef.current = focused;
          } else {
            return;
          }
        }
      }

      let active = activeIndexRef.current ?? anchorIndexRef.current!;
      active = event.key === 'ArrowDown' ? active + 1 : active - 1;

      const totalCount = grid.totalCount();
      if (active < 0) active = 0;
      if (totalCount >= 0 && active >= totalCount) active = totalCount - 1;

      activeIndexRef.current = active;

      const anchor = anchorIndexRef.current!;
      const start = Math.min(anchor, active);
      const end = Math.max(anchor, active);

      const keys: any[] = [];
      for (let i = start; i <= end; i++) {
        const key = grid.getKeyByRowIndex(i);
        if (key !== undefined) keys.push(key);
      }

      if (keys.length > 0) {
        grid.selectRows(keys, false);
        // 通过索引设置焦点,替代DOM元素聚焦
        grid.option('focusedRowIndex', active);
        // 确保聚焦行滚动到可视区域
        grid.scrollToRow(active);
      }
    };

    document.addEventListener('keydown', onKeyDown, true);
    return () => {
      document.removeEventListener('keydown', onKeyDown, true);
    };
  }, []);

  return (
    <div ref={wrapperRef}>
      <DataGrid
        height={440}
        dataSource={dataSource}
        showBorders={true}
        remoteOperations={true}
        wordWrapEnabled={true}
        onInitialized={handleInitialized}
        onRowClick={handleRowClick}
        focusedRowEnabled={true} // 开启聚焦行功能
      >
        <Selection mode="multiple" showCheckBoxesMode="none" />
        <Scrolling mode="virtual" rowRenderingMode="virtual" />
        <Paging defaultPageSize={100} />
        <HeaderFilter visible={true}>
          <Search enabled={true} />
        </HeaderFilter>
        <Column dataField="Id" width={90} />
        <Column dataField="StoreName" caption="Store" width={150} />
        <Column dataField="ProductCategoryName" caption="Category" width={120} />
        <Column dataField="ProductName" caption="Product" />
        <Column dataField="DateKey" caption="Date" dataType="date" format="yyyy-MM-dd" width={110} />
        <Column dataField="SalesAmount" caption="Amount" format="currency" width={100}>
          <HeaderFilter groupInterval={1000} />
        </Column>
      </DataGrid>
    </div>
  );
};

export default App;

关键修改点

  • 开启focusedRowEnabled配置,启用内置聚焦行管理
  • 用grid.option('focusedRowIndex', active)替代grid.focus(cell),通过索引维护焦点
  • 添加grid.scrollToRow(active),确保聚焦行始终在可视区域内
  • 在点击事件中同步更新聚焦行索引,保持选中状态与焦点的一致性

内容的提问来源于stack exchange,提问作者Jan Michálek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.02 01:02:27