Fix spreadsheet keyboard focus: active cell on entry, F6 cycling, grid tab stop

- Default the active sheet to A1 when it has no selection, centralized in a single
  SetActiveSheet method (covers load, sheet switch, add and delete). Without an active
  cell the focused grid showed no indicator and the toolbar tools stayed disabled, so
  F6 into the toolbar found nothing focusable and got stuck.
- focusRegion now skips hidden focusables (inline-rendered but closed color-picker and
  numeric-stepper popups), so F6 can no longer be stranded on the ribbon.
- Give the virtual-grid scroller tabindex=-1 so Chrome's focusable-scroll-container
  behavior stops turning it into a phantom tab stop that scrolls but never navigates.
This commit is contained in:
Atanas Korchev
2026-06-30 12:18:21 +03:00
parent 271b3a23e4
commit 162abbda27
3 changed files with 29 additions and 18 deletions

View File

@@ -429,7 +429,17 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
{
workbook = value;
workbookView = null;
sheetIndex = 0;
SetActiveSheet(0);
}
private void SetActiveSheet(int index)
{
sheetIndex = index;
if (Worksheet?.Selection.Cell == CellRef.Invalid)
{
sheet.Selection.Select(new CellRef(0, 0));
}
}
private async Task CloseMenusAsync()
@@ -451,7 +461,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
await CloseMenusAsync();
sheetIndex = index;
SetActiveSheet(index);
}
private async Task OnAddSheetAsync()
@@ -465,7 +475,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
var name = GenerateSheetName();
workbook.AddSheet(name, 100, 26);
sheetIndex = workbook.Sheets.Count - 1;
SetActiveSheet(workbook.Sheets.Count - 1);
}
private async Task OnSheetAction(RadzenSplitButtonItem? item, Worksheet sheet)
@@ -505,14 +515,12 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
workbook.RemoveSheet(sheet);
workbookView?.Remove(sheet);
if (sheetIndex >= workbook.Sheets.Count)
{
sheetIndex = workbook.Sheets.Count - 1;
}
else if (removedIndex < sheetIndex)
{
sheetIndex--;
}
// Re-activate even when the index stays the same: deleting the active, non-last sheet shifts a
// different sheet into that index, which still needs its active cell ensured.
SetActiveSheet(
sheetIndex >= workbook.Sheets.Count ? workbook.Sheets.Count - 1 :
removedIndex < sheetIndex ? sheetIndex - 1 :
sheetIndex);
}
private async Task OnRenameSheetAsync(Worksheet sheet)
@@ -695,8 +703,6 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
}
}
// Builds a sort command scoped to the data the clicked cell belongs to, never including a
// table/auto-filter header (or table totals) row in the sorted range.
private SortCommand BuildSortCommand(int row, int column, SortOrder order)
{
foreach (var table in Worksheet!.Tables)

View File

@@ -13,7 +13,7 @@
}
}
</div>
<div class="rz-virtual-grid-scroller" @ref=@scrollable>
<div class="rz-virtual-grid-scroller" tabindex="-1" @ref=@scrollable>
<div class="rz-virtual-grid-spacer" style=@SpacerStyle></div>
</div>
</div>

View File

@@ -6065,8 +6065,9 @@ class Spreadsheet {
this.dotNetRef.invokeMethodAsync('OnKeyDownAsync', this.toEventArgs(e), isGridContext);
}
// F6 / Shift+F6 cycle focus between the spreadsheet regions: ribbon tabs -> active toolbar ->
// formula bar -> grid -> sheet tabs. This is the advertised escape from the grid (WCAG 2.1.2).
// F6 / Shift+F6 cycle focus between the spreadsheet regions in their visible top-to-bottom order:
// ribbon tabs -> active toolbar -> formula bar -> grid -> sheet tabs. This is the advertised escape
// from the grid (WCAG 2.1.2).
focusAdjacent = (forward) => {
// this.element IS this spreadsheet's root (.rz-spreadsheet, role=application). querySelector
// only matches descendants of root, so regions from another spreadsheet on the page are never
@@ -6114,8 +6115,12 @@ class Spreadsheet {
return;
}
const focusable = region.querySelector('button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]');
(focusable || region).focus();
// Skip hidden focusables: inline-rendered but closed popups (color pickers, numeric steppers)
// match the selector yet calling focus() on a hidden element silently fails, which would strand
// the user on the previous region. Pick the first one that is actually rendered.
const focusables = region.querySelectorAll('button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]');
const target = [...focusables].find(el => el.offsetParent !== null || el.getClientRects().length > 0);
(target || region).focus();
}
// Opens the cell context menu at the active cell (Shift+F10 / ContextMenu key). The cell element is