Skip to content

Commit 58a7667

Browse files
authored
DataGrid: improve navigation with arrow keys (#6490)
1 parent 3ef70bb commit 58a7667

3 files changed

Lines changed: 160 additions & 54 deletions

File tree

Source/Extensions/Blazorise.DataGrid/Enums/DataGridNavigationMode.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ public enum DataGridNavigationMode
1111
Default,
1212

1313
/// <summary>
14-
/// Cell navigation mode, allows grid cells navigation by pressing the Keyboard's ArrowLeft, ArrowUp, ArrowRight and ArrowDown keys.
14+
/// Cell navigation mode, allows grid cells navigation by pressing the Keyboard's ArrowLeft, ArrowUp, ArrowRight, ArrowDown, Home, End, PageUp and PageDown keys.
1515
/// </summary>
1616
Cell,
1717

1818
/// <summary>
19-
/// Row navigation mode, allows grid row navigation by pressing the Keyboard's ArrowUp and ArrowDown keys.
19+
/// Row navigation mode, allows grid row navigation by pressing the Keyboard's ArrowUp, ArrowDown, Home, End, PageUp and PageDown keys.
2020
/// </summary>
2121
Row,
22-
}
22+
}

Source/Extensions/Blazorise.DataGrid/Internal/_DataGridRow.razor.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ protected internal async Task HandleKeyDown( KeyboardEventArgs eventArgs )
181181
case "PageDown" when idx < lastIndex:
182182
targetIndex = Math.Min( lastIndex, idx + pageSize );
183183
break;
184+
case "Home" when idx > 0:
185+
targetIndex = 0;
186+
break;
187+
case "End" when idx < lastIndex:
188+
targetIndex = lastIndex;
189+
break;
184190
default:
185191
return;
186192
}

Source/Extensions/Blazorise.DataGrid/wwwroot/datagrid.js

Lines changed: 151 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,81 @@ function findAncestorByTagName(el, tagName) {
173173
return el;
174174
}
175175

176+
function isFocusableCell(cell) {
177+
return cell && cell.getAttribute("tabindex") == 0;
178+
}
179+
180+
function focusCell(cell) {
181+
if (!isFocusableCell(cell))
182+
return false;
183+
184+
cell.focus({ focusVisible: true });
185+
return true;
186+
}
187+
188+
function getBodyRows(table) {
189+
return Array.from(table.querySelectorAll("tbody tr"));
190+
}
191+
192+
function getRowCells(row) {
193+
return row ? Array.from(row.querySelectorAll("td")) : [];
194+
}
195+
196+
function getNavigableCells(table) {
197+
return Array.from(table.querySelectorAll(QUERYSELECTOR_ALL_COLUMNS)).filter(isFocusableCell);
198+
}
199+
200+
function findClosestNavigableCell(rowCells, preferredIndex) {
201+
if (!rowCells || rowCells.length === 0)
202+
return null;
203+
204+
const startIndex = Math.max(0, Math.min(preferredIndex, rowCells.length - 1));
205+
206+
for (let offset = 0; offset < rowCells.length; offset++) {
207+
const leftIndex = startIndex - offset;
208+
if (leftIndex >= 0 && isFocusableCell(rowCells[leftIndex]))
209+
return rowCells[leftIndex];
210+
211+
const rightIndex = startIndex + offset;
212+
if (offset > 0 && rightIndex < rowCells.length && isFocusableCell(rowCells[rightIndex]))
213+
return rowCells[rightIndex];
214+
}
215+
216+
return null;
217+
}
218+
219+
function getPageJumpRowCount(table, rows) {
220+
if (!rows || rows.length === 0)
221+
return 1;
222+
223+
const container = findScrollContainer(table);
224+
if (!container)
225+
return 1;
226+
227+
let totalHeight = 0;
228+
let measuredRows = 0;
229+
230+
for (const row of rows) {
231+
const rowHeight = row.offsetHeight;
232+
if (!rowHeight)
233+
continue;
234+
235+
totalHeight += rowHeight;
236+
measuredRows += 1;
237+
}
238+
239+
if (measuredRows === 0)
240+
return 1;
241+
242+
const averageRowHeight = totalHeight / measuredRows;
243+
const containerHeight = container.clientHeight || table.parentElement?.clientHeight || 0;
244+
245+
if (!averageRowHeight || !containerHeight)
246+
return 1;
247+
248+
return Math.max(1, Math.floor(containerHeight / averageRowHeight));
249+
}
250+
176251
function clickCellNavigation(e) {
177252
// Do not hijack clicks coming from interactive elements
178253
const interactiveClosest = e.target.closest('input,select,textarea,button,label,a,[role="button"],[role="checkbox"],[contenteditable="true"]');
@@ -209,24 +284,32 @@ function KeyDownCellNavigation(e) {
209284
}
210285
const TAG_NAMES_INPUT = ["INPUT", "SELECT", "TEXTAREA"];
211286
const TAG_NAME_TABLE_COLUMN = "TD";
212-
const QUERYSELECTOR_FIRST_ROW_COLUMNS = "tbody tr:first-child td";
213287

214-
let isLeft = e.keyCode == 37;
215-
let isUp = e.keyCode == 38;
216-
let isRight = e.keyCode == 39;
217-
let isDown = e.keyCode == 40;
218-
let isArrow = isLeft | isUp | isRight | isDown;
288+
let isLeft = e.key === "ArrowLeft" || e.keyCode == 37;
289+
let isUp = e.key === "ArrowUp" || e.keyCode == 38;
290+
let isRight = e.key === "ArrowRight" || e.keyCode == 39;
291+
let isDown = e.key === "ArrowDown" || e.keyCode == 40;
292+
let isPageUp = e.key === "PageUp" || e.keyCode == 33;
293+
let isPageDown = e.key === "PageDown" || e.keyCode == 34;
294+
let isEnd = e.key === "End" || e.keyCode == 35;
295+
let isHome = e.key === "Home" || e.keyCode == 36;
296+
let isArrow = isLeft || isUp || isRight || isDown;
297+
let isNavigationKey = isArrow || isPageUp || isPageDown || isHome || isEnd;
219298
let isEnterKey = e.keyCode == 13;
220299
let isEscKey = e.keyCode == 27;
221300

222301
let focusedElement = document.activeElement;
223-
let allCells = element.querySelectorAll(QUERYSELECTOR_ALL_COLUMNS);
302+
let allCells = Array.from(element.querySelectorAll(QUERYSELECTOR_ALL_COLUMNS));
303+
let navigableCells = getNavigableCells(element);
304+
let focusedCell = focusedElement?.tagName === TAG_NAME_TABLE_COLUMN
305+
? focusedElement
306+
: focusedElement?.closest(TAG_NAME_TABLE_COLUMN.toLowerCase());
224307

225-
if (!allCells || allCells.length == 0) {
308+
if (!allCells || allCells.length == 0 || !navigableCells || navigableCells.length == 0) {
226309
return;
227310
}
228311

229-
let index = [].indexOf.call(allCells, focusedElement);
312+
let index = focusedCell ? allCells.indexOf(focusedCell) : -1;
230313
let isInputFocused = focusedElement && TAG_NAMES_INPUT.includes(focusedElement.tagName);
231314

232315
if (isInputFocused && (isEnterKey || isEscKey)) {
@@ -237,11 +320,9 @@ function KeyDownCellNavigation(e) {
237320
if (!inputStillExists) {
238321

239322
let tdElement = findAncestorByTagName(focusedElement, TAG_NAME_TABLE_COLUMN);
240-
241-
let index = [].indexOf.call(allCells, tdElement);
242-
let toFocus = element.querySelectorAll(QUERYSELECTOR_ALL_COLUMNS)[index - 1];
243-
if (toFocus && toFocus.getAttribute("tabindex") == 0) {
244-
toFocus.focus();
323+
let tdIndex = tdElement ? allCells.indexOf(tdElement) : -1;
324+
let toFocus = tdIndex > 0 ? allCells[tdIndex - 1] : null;
325+
if (focusCell(toFocus)) {
245326
return;
246327
}
247328
}
@@ -250,69 +331,88 @@ function KeyDownCellNavigation(e) {
250331
});
251332
}
252333

334+
if (isInputFocused) {
335+
return;
336+
}
337+
253338
if (index == -1) {
254-
if (isArrow && !isInputFocused) {
255-
while (index < allCells.length - 1) {
256-
let toFocus = allCells[index + 1];
257-
if (toFocus.getAttribute("tabindex") == 0) {
258-
toFocus.focus();
259-
return;
260-
}
261-
index += 1;
262-
}
339+
if (isNavigationKey) {
340+
let initialCell = isEnd ? navigableCells[navigableCells.length - 1] : navigableCells[0];
341+
focusCell(initialCell);
263342
}
264343
return;
265344
}
266345

267-
if (isArrow) {
346+
if (isNavigationKey) {
268347
e.preventDefault();
269348
}
270349

350+
let currentRow = focusedCell?.closest("tr");
351+
let rows = getBodyRows(element).filter(row => getRowCells(row).some(isFocusableCell));
352+
let rowIndex = currentRow ? rows.indexOf(currentRow) : -1;
353+
let rowCells = getRowCells(currentRow);
354+
let cellIndex = rowCells.indexOf(focusedCell);
355+
let navigableCellIndex = navigableCells.indexOf(focusedCell);
356+
357+
if (rowIndex < 0 || cellIndex < 0 || navigableCellIndex < 0) {
358+
return;
359+
}
271360

272361
if (isLeft) {
273-
while (index > 0) {
274-
let toFocus = allCells[index - 1];
275-
if (toFocus.getAttribute("tabindex") == 0) {
276-
toFocus.focus();
277-
return;
278-
}
279-
index -= 1;
280-
}
362+
focusCell(navigableCells[navigableCellIndex - 1]);
281363

282364
return;
283365
}
284366

285367
if (isUp) {
286-
let rowCount = element.querySelectorAll(QUERYSELECTOR_FIRST_ROW_COLUMNS).length;
287-
let toFocus = allCells[index - rowCount];
288-
if (toFocus && toFocus.getAttribute("tabindex") == 0) {
289-
toFocus.focus();
290-
}
368+
let targetRowIndex = Math.max(0, rowIndex - 1);
369+
let toFocus = findClosestNavigableCell(getRowCells(rows[targetRowIndex]), cellIndex);
370+
focusCell(toFocus);
291371
return;
292372
}
293373

294374
if (isRight) {
295-
296-
while (index < allCells.length - 1) {
297-
let toFocus = allCells[index + 1];
298-
if (toFocus.getAttribute("tabindex") == 0) {
299-
toFocus.focus();
300-
return;
301-
}
302-
index += 1;
303-
}
375+
focusCell(navigableCells[navigableCellIndex + 1]);
304376

305377
return;
306378
}
307379

308380
if (isDown) {
309-
let rowCount = element.querySelectorAll(QUERYSELECTOR_FIRST_ROW_COLUMNS).length;
310-
let toFocus = allCells[index + rowCount];
311-
if (toFocus && toFocus.getAttribute("tabindex") == 0) {
312-
toFocus.focus();
313-
}
381+
let targetRowIndex = Math.min(rows.length - 1, rowIndex + 1);
382+
let toFocus = findClosestNavigableCell(getRowCells(rows[targetRowIndex]), cellIndex);
383+
focusCell(toFocus);
314384
return;
315385
}
316386

387+
if (isPageUp) {
388+
let pageJump = getPageJumpRowCount(element, rows);
389+
let targetRowIndex = Math.max(0, rowIndex - pageJump);
390+
let toFocus = findClosestNavigableCell(getRowCells(rows[targetRowIndex]), cellIndex);
391+
focusCell(toFocus);
392+
return;
393+
}
317394

395+
if (isPageDown) {
396+
let pageJump = getPageJumpRowCount(element, rows);
397+
let targetRowIndex = Math.min(rows.length - 1, rowIndex + pageJump);
398+
let toFocus = findClosestNavigableCell(getRowCells(rows[targetRowIndex]), cellIndex);
399+
focusCell(toFocus);
400+
return;
401+
}
402+
403+
if (isHome) {
404+
let toFocus = e.ctrlKey || e.metaKey
405+
? navigableCells[0]
406+
: rowCells.find(isFocusableCell);
407+
focusCell(toFocus);
408+
return;
409+
}
410+
411+
if (isEnd) {
412+
let toFocus = e.ctrlKey || e.metaKey
413+
? navigableCells[navigableCells.length - 1]
414+
: [...rowCells].reverse().find(isFocusableCell);
415+
focusCell(toFocus);
416+
return;
417+
}
318418
}

0 commit comments

Comments
 (0)