From 162abbda27a674d065618509d1262bb1bcda27f7 Mon Sep 17 00:00:00 2001 From: Atanas Korchev Date: Tue, 30 Jun 2026 12:18:21 +0300 Subject: [PATCH] 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. --- Radzen.Blazor/RadzenSpreadsheet.razor.cs | 32 ++++++++++++--------- Radzen.Blazor/Spreadsheet/VirtualGrid.razor | 2 +- Radzen.Blazor/wwwroot/Radzen.Blazor.js | 13 ++++++--- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/Radzen.Blazor/RadzenSpreadsheet.razor.cs b/Radzen.Blazor/RadzenSpreadsheet.razor.cs index 7eddaa9eb..0d32267b6 100644 --- a/Radzen.Blazor/RadzenSpreadsheet.razor.cs +++ b/Radzen.Blazor/RadzenSpreadsheet.razor.cs @@ -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) diff --git a/Radzen.Blazor/Spreadsheet/VirtualGrid.razor b/Radzen.Blazor/Spreadsheet/VirtualGrid.razor index 4ed1b3f3a..e2b45a528 100644 --- a/Radzen.Blazor/Spreadsheet/VirtualGrid.razor +++ b/Radzen.Blazor/Spreadsheet/VirtualGrid.razor @@ -13,7 +13,7 @@ } } -
+
\ No newline at end of file diff --git a/Radzen.Blazor/wwwroot/Radzen.Blazor.js b/Radzen.Blazor/wwwroot/Radzen.Blazor.js index 58fc07e4f..9ed9f6430 100644 --- a/Radzen.Blazor/wwwroot/Radzen.Blazor.js +++ b/Radzen.Blazor/wwwroot/Radzen.Blazor.js @@ -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