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
相关产品推荐
相关产品推荐

