I've added listeners to my app that are supposed to select a row and, when I press Shift + Up or Down Arrow, select the next rows. It works fine until I reach the bottom or top edge, at which point the focus is lost.
I can't load everything at once because we have about 8,000 records in the DataGrid in production; I have to load them using scrolling.mode: virtual.
How can this be handled in DevExtreme?
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 = () => {
// Instance gridu - uložená přes onInitialized
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;
console.log('[Init] grid instance saved:', !!e.component);
console.log('[Init] has selectRows:', typeof e.component?.selectRows);
console.log('[Init] has getSelectedRowKeys:', typeof e.component?.getSelectedRowKeys);
}, []);
const handleRowClick = useCallback((e: any) => {
const grid = gridInstanceRef.current;
if (!grid) {
console.log('[RowClick] no grid');
return;
}
if (e.rowType !== 'data') return;
const rowIndex = e.rowIndex;
const nativeEvent = e.event;
console.log('[RowClick] rowIndex:', rowIndex, 'shift:', nativeEvent?.shiftKey);
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;
} else {
// selectRowsByIndexes nemusí existovat - použijeme getKeyByRowIndex + selectRows
const key = grid.getKeyByRowIndex(rowIndex);
if (key !== undefined) {
grid.selectRows([key], false);
}
anchorIndexRef.current = rowIndex;
activeIndexRef.current = rowIndex;
}
}, []);
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) return;
const onKeyDown = (event: KeyboardEvent) => {
if (!event.shiftKey) return;
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
const grid = gridInstanceRef.current;
if (!grid) {
console.log('[KeyDown] no grid instance');
return;
}
const target = event.target as HTMLElement;
if (!wrapper.contains(target)) return;
const tag = target?.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || target?.isContentEditable) return;
// Zastav DŘÍV než DX naviguje
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
// Resolve anchor
if (anchorIndexRef.current === null) {
const selected = grid.getSelectedRowKeys();
console.log('[KeyDown] anchor null, selected:', selected);
if (selected && 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 {
console.log('[KeyDown] no anchor available');
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);
console.log('[KeyDown] anchor:', anchor, 'active:', active, 'range:', start, '-', end);
const keys: any[] = [];
for (let i = start; i <= end; i++) {
const k = grid.getKeyByRowIndex(i);
if (k !== undefined) keys.push(k);
}
console.log('[KeyDown] keys to select:', keys.length);
if (keys.length > 0) {
grid.selectRows(keys, false);
const cell = grid.getCellElement(active, 0);
if (cell) {
grid.focus(cell);
}
}
};
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}
>
<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;