Add screen-reader and keyboard accessibility to RadzenSpreadsheet

Make the spreadsheet usable with a screen reader and fully operable by keyboard,
keeping the virtualized cell hot path perf-neutral, with no breaking change
beyond the intended one (the toolbar becomes Tab-navigable).

- role="application" on the root + an isolated, self-rendering live-region
  announcer (RadzenLiveRegion + SpreadsheetAccessibility) that speaks the active
  cell (address, value, frozen-row header, position, state), selection size, and
  structural actions (sort/filter/insert) without re-rendering the grid.
- Contextual keyboard gate: Tab moves the active cell in the grid but stays native
  in the chrome; F6/Shift+F6 cycle regions (ribbon/toolbar/formula bar/grid/sheet
  tabs) - the WCAG 2.1.2 escape. Full nav set added: Home/End, Ctrl+Home/End,
  Ctrl+A, Ctrl+Arrow edge-jump, Ctrl+Space/Shift+Space, PageUp/Down, Ctrl+D/R fill;
  RTL arrow swap; IME/"Home types H" guard; F2, Ctrl+Y; Shift+F10 context menu.
- Accessible names on every toolbar tool + SelectBar item (localized via nameof);
  inner editor gets role=textbox + a mode-specific label via InputAttributes.
- Image/chart keyboard layer (Ctrl+Alt+5 select/cycle, arrows move, Size dialog
  resize), undoable column/row resize, Alt+/ shortcut-help dialog, dialog focus
  restore.
- 19 new bUnit/xUnit tests; full suite green; verified live in Chrome.
This commit is contained in:
Atanas Korchev
2026-06-27 23:18:15 +03:00
committed by Atanas Korchev
parent e17f1113e7
commit 8befb201ef
51 changed files with 1766 additions and 70 deletions

View File

@@ -0,0 +1,315 @@
using System.Threading.Tasks;
using Bunit;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.JSInterop;
using Radzen.Blazor;
using Radzen.Blazor.Rendering;
using Radzen.Blazor.Spreadsheet;
using Radzen.Documents.Spreadsheet;
using Xunit;
namespace Radzen.Blazor.Tests.Spreadsheet;
public class SpreadsheetAccessibilityTests
{
private static TestContext CreateContext()
{
var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.Setup<VirtualRegion>("Radzen.createVirtualItemContainer", _ => true)
.SetResult(new VirtualRegion { Width = 800, Height = 600, ScrollWidth = 800, ScrollHeight = 600 });
ctx.Services.AddRadzenComponents();
return ctx;
}
private static (Workbook Workbook, Worksheet Sheet) NewWorkbook()
{
var wb = new Workbook();
var sheet = wb.AddSheet("Sheet1", 10, 10);
return (wb, sheet);
}
// A test localizer: friendly text for the keys the announcer composes, and the position format.
private static string L(string key) => key switch
{
nameof(RadzenStrings.Spreadsheet_A11yBlank) => "blank",
nameof(RadzenStrings.Spreadsheet_A11yPosition) => "row {0} of {1}, column {2} of {3}",
nameof(RadzenStrings.Spreadsheet_A11yMerged) => "merged",
nameof(RadzenStrings.Spreadsheet_A11yFormula) => "has formula",
_ => key
};
// ── Announcer compose (pure function) ───────────────────────────────
[Fact]
public void BuildAnnouncement_EmptyCell_AddressBlankAndPosition()
{
var (_, sheet) = NewWorkbook();
var text = SpreadsheetAccessibility.BuildAnnouncement(sheet, new CellRef(0, 0), L);
Assert.StartsWith("A1, blank", text);
Assert.Contains("row 1 of 10, column A of 10", text);
}
[Fact]
public void BuildAnnouncement_Position_IsOneBasedWithColumnLetter()
{
var (_, sheet) = NewWorkbook();
var text = SpreadsheetAccessibility.BuildAnnouncement(sheet, new CellRef(2, 2), L);
Assert.StartsWith("C3,", text);
Assert.Contains("row 3 of 10, column C of 10", text);
}
[Fact]
public void BuildAnnouncement_NullWorksheet_ReturnsEmpty()
{
Assert.Equal(string.Empty, SpreadsheetAccessibility.BuildAnnouncement(null!, new CellRef(0, 0), L));
}
[Fact]
public void BuildAnnouncement_InvalidCell_ReturnsEmpty()
{
var (_, sheet) = NewWorkbook();
Assert.Equal(string.Empty, SpreadsheetAccessibility.BuildAnnouncement(sheet, CellRef.Invalid, L));
}
// ── Root application region + instructions (WI1) ────────────────────
[Fact]
public void Root_Has_ApplicationRole_Keyshortcuts_And_Describedby()
{
using var ctx = CreateContext();
var (wb, _) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
Assert.Contains("role=\"application\"", c.Markup);
Assert.Contains("aria-keyshortcuts=\"F6 Shift+F6\"", c.Markup);
Assert.Contains("-a11y-instructions", c.Markup); // aria-describedby target + the span id
}
// ── Toolbar group + tool names (WI5) ────────────────────────────────
[Fact]
public void Toolbar_Has_GroupRole_And_NamedTools()
{
using var ctx = CreateContext();
var (wb, _) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
Assert.Contains("role=\"group\"", c.Markup);
Assert.Contains("aria-label=\"Bold\"", c.Markup); // RadzenToggleButton AriaLabel
Assert.Contains("aria-label=\"Undo\"", c.Markup); // RadzenButton splatted aria-label
}
// ── Editor naming (WI6) ─────────────────────────────────────────────
[Fact]
public void FormulaBar_Editor_Has_TextboxRole_And_Name()
{
using var ctx = CreateContext();
var (wb, _) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
Assert.Contains("role=\"textbox\"", c.Markup);
Assert.Contains("aria-label=\"Formula bar\"", c.Markup);
}
// ── Contextual keyboard gate (WI2) ──────────────────────────────────
[Fact]
public async Task Gate_TabInGridContext_MovesActiveCell()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(0, 0)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "Tab", Code = "Tab" }, true));
Assert.Equal(1, sheet.Selection.Cell.Column);
}
[Fact]
public async Task Gate_TabOutsideGridContext_DoesNotMoveActiveCell()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(0, 0)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "Tab", Code = "Tab" }, false));
Assert.Equal(0, sheet.Selection.Cell.Column);
}
// ── Extended navigation keys (WI2) ──────────────────────────────────
[Fact]
public async Task NavKey_CtrlHome_MovesToA1()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(2, 2)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "Home", Code = "Home", CtrlKey = true }, true));
Assert.Equal(0, sheet.Selection.Cell.Row);
Assert.Equal(0, sheet.Selection.Cell.Column);
}
[Fact]
public async Task NavKey_Home_MovesToRowStart()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(2, 2)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "Home", Code = "Home" }, true));
Assert.Equal(2, sheet.Selection.Cell.Row);
Assert.Equal(0, sheet.Selection.Cell.Column);
}
[Fact]
public async Task NavKey_CtrlA_SelectsFromA1()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(2, 2)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "a", Code = "KeyA", CtrlKey = true }, true));
Assert.Equal(0, sheet.Selection.Range.Start.Row);
Assert.Equal(0, sheet.Selection.Range.Start.Column);
}
[Fact]
public async Task NavKey_GridOnly_DoesNotActOutsideGrid()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(2, 2)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "Home", Code = "Home", CtrlKey = true }, false));
Assert.Equal(2, sheet.Selection.Cell.Column); // Ctrl+Home is grid-only; blocked outside the grid
}
[Fact]
public async Task NavKey_CtrlDown_EmptySheet_JumpsToLastRow()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook(); // 10x10
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(0, 0)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = "ArrowDown", Code = "ArrowDown", CtrlKey = true }, true));
Assert.Equal(9, sheet.Selection.Cell.Row); // edge jump to the last row of a 10-row sheet
Assert.Equal(0, sheet.Selection.Cell.Column);
}
[Fact]
public async Task NavKey_CtrlSpace_SelectsWholeColumn()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook(); // 10x10
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
await c.InvokeAsync(() => sheet.Selection.Select(new CellRef(2, 2)));
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(new KeyboardEventArgs { Key = " ", Code = "Space", CtrlKey = true }, true));
Assert.Equal(2, sheet.Selection.Range.Start.Column);
Assert.Equal(2, sheet.Selection.Range.End.Column);
Assert.Equal(0, sheet.Selection.Range.Start.Row);
Assert.Equal(9, sheet.Selection.Range.End.Row); // full column of a 10-row sheet
}
// ── Shortcut help dialog (Alt+/) ────────────────────────────────────
[Fact]
public void ShortcutsDialog_Renders_RowsInTable()
{
using var ctx = CreateContext();
var rows = new System.Collections.Generic.List<SpreadsheetShortcutsDialog.Shortcut>
{
new("F6", "Move between regions"),
new("Ctrl+A", "Select the used range")
};
var c = ctx.RenderComponent<SpreadsheetShortcutsDialog>(p => p
.Add(x => x.Shortcuts, rows)
.Add(x => x.ShortcutColumn, "Shortcut")
.Add(x => x.ActionColumn, "Action"));
Assert.Contains("rz-datatable", c.Markup); // RadzenTable, not a naked <table>
Assert.Contains("<kbd>F6</kbd>", c.Markup);
Assert.Contains("Move between regions", c.Markup);
Assert.Contains("Select the used range", c.Markup);
}
// ── Image/chart keyboard layer (WI7) ────────────────────────────────
[Fact]
public async Task Drawing_CtrlAlt5Selects_ArrowMoves_EscapeDeselects()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var image = new SheetImage
{
AnchorMode = DrawingAnchorMode.OneCellAnchor,
From = new CellAnchor { Column = 1, ColumnOffset = 0, Row = 1, RowOffset = 0 },
Width = 100,
Height = 100
};
sheet.AddImage(image);
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
// Ctrl+Alt+5 enters the drawing layer and selects the first image.
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(
new KeyboardEventArgs { Key = "5", Code = "Digit5", CtrlKey = true, AltKey = true }, true));
Assert.Same(image, sheet.SelectedImage);
// ArrowRight moves it (one undoable command); the anchor advances.
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(
new KeyboardEventArgs { Key = "ArrowRight", Code = "ArrowRight" }, true));
Assert.True(image.From.ColumnOffset > 0 || image.From.Column > 1);
// Escape returns to the grid.
await c.InvokeAsync(() => c.Instance.OnKeyDownAsync(
new KeyboardEventArgs { Key = "Escape", Code = "Escape" }, true));
Assert.Null(sheet.SelectedImage);
}
// ── Column/row resize undo (in scope) ───────────────────────────────
[Fact]
public async Task ResizeColumn_DragPushesOneUndoableCommand()
{
using var ctx = CreateContext();
var (wb, sheet) = NewWorkbook();
var c = ctx.RenderComponent<RadzenSpreadsheet>(p => p.Add(x => x.Workbook, wb));
var startWidth = sheet.Columns[2];
await c.InvokeAsync(() => c.Instance.OnColumnResizePointerDownAsync(
new CellEventArgs { Column = 2, Pointer = new PointerEventArgs { ClientX = 100 } }));
await c.InvokeAsync(() => c.Instance.OnColumnResizePointerMoveAsync(new PointerEventArgs { ClientX = 160 }));
await c.InvokeAsync(() => c.Instance.OnColumnResizePointerUpAsync(new PointerEventArgs { ClientX = 160 }));
Assert.True(sheet.Columns[2] > startWidth); // drag widened it, applied via the command
await c.InvokeAsync(() => c.Instance.Undo());
Assert.Equal(startWidth, sheet.Columns[2]); // Ctrl+Z reverts the resize
}
}

View File

@@ -0,0 +1,23 @@
@namespace Radzen.Blazor
@*
A visually-hidden ARIA live region. The element is rendered on first render with empty text so
assistive technology registers it before any message is injected (injecting a live region together
with its first message is a common WCAG 4.1.3 Status Messages failure). Set Text to announce.
*@
<div role=@(Assertive ? "alert" : "status") aria-live=@(Assertive ? "assertive" : "polite") aria-atomic="true"
style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border:0">@Text</div>
@code {
/// <summary>
/// The message to announce. Assigning a new (different) value triggers an announcement.
/// </summary>
[Parameter]
public string? Text { get; set; }
/// <summary>
/// When <c>true</c> the region is <c>role="alert"</c> / <c>aria-live="assertive"</c> (interrupts the
/// user). Default is a polite <c>role="status"</c> region.
/// </summary>
[Parameter]
public bool Assertive { get; set; }
}

View File

@@ -23,7 +23,7 @@
<button type="button" @ref="@item.Element" id="@item.GetItemId()"
@onclick="@(args => SelectItem(item))"
@attributes="item.Attributes" style="@item.Style" class=@ButtonClass(item)
aria-label="@item.Text" aria-pressed="@(IsSelected(item)? "true" : "false")"
aria-label="@(item.AriaLabel ?? item.Text)" aria-pressed="@(IsSelected(item)? "true" : "false")"
disabled="@(Disabled || item.Disabled)" tabindex="-1">
@if (item.Template != null)
{

View File

@@ -65,6 +65,14 @@ namespace Radzen.Blazor
[Parameter]
public object? Value { get; set; }
/// <summary>
/// Gets or sets the accessible label (<c>aria-label</c>) for the item. Falls back to <see cref="Text"/> when
/// not set. Use this to name icon-only items that have no visible <see cref="Text"/> (WCAG 4.1.2).
/// </summary>
/// <value>The accessible label.</value>
[Parameter]
public string? AriaLabel { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this <see cref="RadzenSelectBarItem"/> is disabled.
/// </summary>

View File

@@ -8,7 +8,9 @@
@inject ContextMenuService ContextMenuService
@inherits RadzenComponent
<div @ref=@Element class=@GetCssClass() style=@Style @attributes=@Attributes tabindex="0">
<div @ref=@Element class=@GetCssClass() style=@Style @attributes=@Attributes tabindex="0"
role="application" aria-label=@(AriaLabel ?? Localize(nameof(RadzenStrings.Spreadsheet_A11yGridLabel)))
aria-keyshortcuts="F6 Shift+F6" aria-describedby=@($"{GetId()}-a11y-instructions")>
<CascadingValue TValue="ISpreadsheet" Value=this IsFixed="true">
<CascadingValue Value=@Worksheet TValue="Worksheet">
<CascadingValue Value=@ActiveView?.Commands TValue="UndoRedoStack">
@@ -23,14 +25,14 @@
else
{
<RadzenTabsItem Text=@Localize(nameof(RadzenStrings.Spreadsheet_FileTab))>
<RadzenStack class="rz-toolbar" Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack class="rz-toolbar" role="group" aria-label=@Localize(nameof(RadzenStrings.Spreadsheet_FileTab)) Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenSpreadsheetOpen CsvImportOptions=@CsvImportOptions />
<div class="rz-toolbar-separator"></div>
<RadzenSpreadsheetSave FileName=@ExportFileName CsvExportOptions=@CsvExportOptions />
</RadzenStack>
</RadzenTabsItem>
<RadzenTabsItem Text=@Localize(nameof(RadzenStrings.Spreadsheet_HomeTab))>
<RadzenStack class="rz-toolbar" Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack class="rz-toolbar" role="group" aria-label=@Localize(nameof(RadzenStrings.Spreadsheet_HomeTab)) Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenSpreadsheetUndo />
<RadzenSpreadsheetRedo />
@@ -64,7 +66,7 @@
</RadzenStack>
</RadzenTabsItem>
<RadzenTabsItem Text=@Localize(nameof(RadzenStrings.Spreadsheet_FormatTab))>
<RadzenStack class="rz-toolbar" Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack class="rz-toolbar" role="group" aria-label=@Localize(nameof(RadzenStrings.Spreadsheet_FormatTab)) Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenSpreadsheetDataFormat/>
<RadzenSpreadsheetIncreaseDecimals/>
<RadzenSpreadsheetDecreaseDecimals/>
@@ -75,7 +77,7 @@
</RadzenStack>
</RadzenTabsItem>
<RadzenTabsItem Text=@Localize(nameof(RadzenStrings.Spreadsheet_InsertTab))>
<RadzenStack class="rz-toolbar" Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack class="rz-toolbar" role="group" aria-label=@Localize(nameof(RadzenStrings.Spreadsheet_InsertTab)) Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenSpreadsheetInsertRowAfter/>
<RadzenSpreadsheetInsertRowBefore/>
@@ -95,12 +97,12 @@
</RadzenStack>
</RadzenTabsItem>
<RadzenTabsItem Text=@Localize(nameof(RadzenStrings.Spreadsheet_ViewTab))>
<RadzenStack class="rz-toolbar" Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack class="rz-toolbar" role="group" aria-label=@Localize(nameof(RadzenStrings.Spreadsheet_ViewTab)) Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenSpreadsheetFreeze/>
</RadzenStack>
</RadzenTabsItem>
<RadzenTabsItem Text=@Localize(nameof(RadzenStrings.Spreadsheet_DataTab))>
<RadzenStack class="rz-toolbar" Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenStack class="rz-toolbar" role="group" aria-label=@Localize(nameof(RadzenStrings.Spreadsheet_DataTab)) Orientation="Orientation.Horizontal" Gap="0.125rem" AlignItems="AlignItems.Center">
<RadzenSpreadsheetAutoFilter/>
<RadzenSpreadsheetCustomSort/>
<div class="rz-toolbar-separator"></div>
@@ -114,6 +116,8 @@
}
@if (Worksheet != null)
{
<span id=@($"{GetId()}-a11y-instructions") style="position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0">@Localize(nameof(RadzenStrings.Spreadsheet_A11yInstructions))</span>
<SpreadsheetAccessibility @ref=@accessibility Worksheet=@Worksheet Spreadsheet=this />
<CascadingValue Value="@CellTypes">
@if (ShowFormulaBar)
{

View File

@@ -175,6 +175,13 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
[Parameter]
public bool ShowFormulaBar { get; set; } = true;
/// <summary>
/// Gets or sets the accessible label (<c>aria-label</c>) announced for the spreadsheet's
/// <c>role="application"</c> region. Defaults to a localized "Spreadsheet".
/// </summary>
[Parameter]
public string? AriaLabel { get; set; }
/// <summary>
/// When <c>true</c> (the default) the sheet tab strip is rendered below the grid.
/// </summary>
@@ -301,6 +308,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
protected override string GetComponentCssClass() => "rz-spreadsheet";
private VirtualGrid? grid;
private Spreadsheet.SpreadsheetAccessibility? accessibility;
private RadzenPopup? cellMenuPopup;
private RadzenPopup? validationListPopup;
private RangePickerBar? rangePickerBar;
@@ -365,7 +373,14 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
}
}
return ActiveView?.Commands.Execute(command) ?? false;
var executed = ActiveView?.Commands.Execute(command) ?? false;
if (executed && DescribeCommand(command) is string message)
{
accessibility?.AnnounceAction(message);
}
return executed;
}
/// <inheritdoc/>
@@ -512,7 +527,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
.Select(s => s.Name)
.ToList();
var name = await DialogService.OpenAsync<Spreadsheet.RenameSheetDialog>(Localize(nameof(RadzenStrings.Spreadsheet_RenameSheetTitle)),
var name = await OpenDialogAsync<Spreadsheet.RenameSheetDialog>(Localize(nameof(RadzenStrings.Spreadsheet_RenameSheetTitle)),
new Dictionary<string, object?> { { "Name", sheet.Name }, { "ExistingNames", existingNames } },
new DialogOptions { Width = "300px" });
@@ -753,7 +768,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
parameters.Add(nameof(FilterDialog.Filter), existingFilter);
}
var result = await DialogService.OpenAsync<FilterDialog>(Localize(nameof(RadzenStrings.Spreadsheet_CustomFilterTitle)), parameters, new DialogOptions
var result = await OpenDialogAsync<FilterDialog>(Localize(nameof(RadzenStrings.Spreadsheet_CustomFilterTitle)), parameters, new DialogOptions
{
Width = "600px",
});
@@ -784,7 +799,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
return;
}
var result = await DialogService.OpenAsync<Spreadsheet.Top10FilterDialog>(
var result = await OpenDialogAsync<Spreadsheet.Top10FilterDialog>(
Localize(nameof(RadzenStrings.Spreadsheet_Top10Filter)),
null,
new DialogOptions { Width = "380px", ShowClose = true });
@@ -863,7 +878,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
{ nameof(FormatCellsDialog.ValueType), cell.ValueType }
};
var result = await DialogService.OpenAsync<FormatCellsDialog>(
var result = await OpenDialogAsync<FormatCellsDialog>(
Localize(nameof(RadzenStrings.Spreadsheet_FormatCellsTitle)), parameters, new DialogOptions { Width = "600px" });
if (result is string formatCode)
@@ -948,6 +963,9 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
result = false;
break;
}
// Return focus to the grid after the validation dialog closes (WCAG 2.4.3).
await Element.FocusAsync();
}
else
{
@@ -988,6 +1006,443 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
}
}
// Absolute-move keys (Home/End/Ctrl+Home/Ctrl+End): the mapper turns the current active cell
// into the destination, which is then clamped to the sheet and selected.
private async Task MoveToAsync(Func<CellRef, CellRef> targetOf)
{
if (Worksheet is null || !await AcceptAsync())
{
return;
}
var target = Worksheet.Clamp(targetOf(Worksheet.Selection.Cell));
Worksheet.Selection.Select(target);
await ScrollToAsync(target);
}
// Shift variants: extend the selection from the active cell to the mapped destination.
private Task ExtendToAsync(Func<CellRef, CellRef> targetOf)
{
if (Worksheet is null)
{
return Task.CompletedTask;
}
var current = Worksheet.Selection.Cell;
var target = Worksheet.Clamp(targetOf(current));
return ExtendSelectionAsync(target.Row - current.Row, target.Column - current.Column);
}
private async Task SelectUsedRangeAsync()
{
if (Worksheet is null || !await AcceptAsync())
{
return;
}
Worksheet.Selection.Select(new RangeRef(new CellRef(0, 0), GetUsedEnd()));
await ScrollToAsync(new CellRef(0, 0));
}
// The bottom-right corner of the populated range, used by Ctrl+End/Ctrl+A/End. Empty sheet -> A1.
private CellRef GetUsedEnd()
{
var maxRow = 0;
var maxColumn = 0;
if (Worksheet is not null)
{
foreach (var cell in Worksheet.Cells.GetPopulatedCells())
{
if (cell.Address.Row > maxRow)
{
maxRow = cell.Address.Row;
}
if (cell.Address.Column > maxColumn)
{
maxColumn = cell.Address.Column;
}
}
}
return new CellRef(maxRow, maxColumn);
}
// Excel-style Ctrl+Arrow edge jump from 'from' in direction (dRow, dColumn), each +/-1 or 0:
// - on a data block: jump to the last contiguous populated cell;
// - otherwise: skip blanks to the next populated cell, or to the sheet edge if none.
private CellRef FindEdge(CellRef from, int dRow, int dColumn)
{
if (Worksheet is null)
{
return from;
}
var rows = Worksheet.RowCount;
var columns = Worksheet.ColumnCount;
var r = from.Row;
var c = from.Column;
bool InBounds(int row, int column) => row >= 0 && row < rows && column >= 0 && column < columns;
bool Has(int row, int column) => Worksheet.Cells.HasCell(row, column);
if (!InBounds(r + dRow, c + dColumn))
{
return from;
}
if (Has(r, c) && Has(r + dRow, c + dColumn))
{
while (InBounds(r + dRow, c + dColumn) && Has(r + dRow, c + dColumn))
{
r += dRow;
c += dColumn;
}
}
else
{
var found = false;
while (InBounds(r + dRow, c + dColumn))
{
r += dRow;
c += dColumn;
if (Has(r, c))
{
found = true;
break;
}
}
if (!found)
{
while (InBounds(r + dRow, c + dColumn))
{
r += dRow;
c += dColumn;
}
}
}
return new CellRef(r, c);
}
// Ctrl+D fills the multi-cell selection down from its top row; Ctrl+R fills right from its first column.
private async Task FillAsync(bool down)
{
if (Worksheet is null)
{
return;
}
var range = Worksheet.Selection.Range;
if (range.Collapsed)
{
return;
}
RangeRef source;
AutofillDirection direction;
if (down)
{
source = new RangeRef(range.Start, new CellRef(range.Start.Row, range.End.Column));
direction = AutofillDirection.Down;
}
else
{
source = new RangeRef(range.Start, new CellRef(range.End.Row, range.Start.Column));
direction = AutofillDirection.Right;
}
await ExecuteAsync(new AutofillCommand(Worksheet, source, range, direction));
}
private async Task SelectColumnAsync()
{
if (Worksheet is null || !await AcceptAsync())
{
return;
}
Worksheet.Selection.Select(new ColumnRef(Worksheet.Selection.Cell.Column));
await ScrollToAsync(Worksheet.Selection.Cell);
}
private async Task SelectRowAsync()
{
if (Worksheet is null || !await AcceptAsync())
{
return;
}
Worksheet.Selection.Select(new RowRef(Worksheet.Selection.Cell.Row));
await ScrollToAsync(Worksheet.Selection.Cell);
}
// Shift+F10 / the ContextMenu key open the cell context menu at the active cell. JS locates the
// cell element and re-dispatches OnCellContextMenuAsync with a synthetic pointer at its position.
private async Task OpenContextMenuAtActiveCellAsync()
{
if (Worksheet is null || jsRef is null || Worksheet.Selection.Cell == CellRef.Invalid)
{
return;
}
var cell = Worksheet.Selection.Cell;
await jsRef.InvokeVoidAsync("openCellContextMenu", cell.Row, cell.Column);
}
// ── Image/chart keyboard layer (Excel object model) ─────────────────
// Ctrl+Alt+5 selects the first drawing, or cycles when already in the layer.
private Task EnterDrawingLayerAsync()
{
if (Worksheet is null)
{
return Task.CompletedTask;
}
if (Worksheet.SelectedImage is null && Worksheet.SelectedChart is null)
{
if (Worksheet.Images.Count > 0)
{
Worksheet.SelectedImage = Worksheet.Images[0];
AnnounceDrawing();
}
else if (Worksheet.Charts.Count > 0)
{
Worksheet.SelectedChart = Worksheet.Charts[0];
AnnounceDrawing();
}
}
else
{
CycleDrawing(1);
}
return Task.CompletedTask;
}
private async Task<bool> TryHandleDrawingKeyAsync(KeyboardEventArgs args)
{
const double step = 8;
const double fine = 1;
switch (TranslateShortcut(args))
{
case "Escape": DeselectDrawing(); return true;
case "Tab": CycleDrawing(1); return true;
case "Shift+Tab": CycleDrawing(-1); return true;
case "Enter": return true; // consume so it does not move the underlying cell
case "Shift+F10":
case "ContextMenu": await OpenDrawingSizeDialogAsync(); return true;
case "ArrowUp": await MoveDrawingAsync(0, -step); return true;
case "ArrowDown": await MoveDrawingAsync(0, step); return true;
case "ArrowLeft": await MoveDrawingAsync(-step, 0); return true;
case "ArrowRight": await MoveDrawingAsync(step, 0); return true;
case "Ctrl+ArrowUp": await MoveDrawingAsync(0, -fine); return true;
case "Ctrl+ArrowDown": await MoveDrawingAsync(0, fine); return true;
case "Ctrl+ArrowLeft": await MoveDrawingAsync(-fine, 0); return true;
case "Ctrl+ArrowRight": await MoveDrawingAsync(fine, 0); return true;
default: return false;
}
}
private void DeselectDrawing()
{
if (Worksheet is null)
{
return;
}
Worksheet.SelectedImage = null;
Worksheet.SelectedChart = null;
}
private void CycleDrawing(int direction)
{
if (Worksheet is null)
{
return;
}
var images = Worksheet.Images;
var charts = Worksheet.Charts;
var total = images.Count + charts.Count;
if (total == 0)
{
return;
}
int current;
if (Worksheet.SelectedImage is SheetImage selectedImage)
{
current = IndexOf(images, selectedImage);
}
else if (Worksheet.SelectedChart is SheetChart selectedChart)
{
current = images.Count + IndexOf(charts, selectedChart);
}
else
{
current = -1;
}
var next = (current + direction + total) % total;
if (next < images.Count)
{
Worksheet.SelectedImage = images[next];
Worksheet.SelectedChart = null;
}
else
{
Worksheet.SelectedChart = charts[next - images.Count];
Worksheet.SelectedImage = null;
}
AnnounceDrawing();
}
private async Task MoveDrawingAsync(double dx, double dy)
{
if (Worksheet is null)
{
return;
}
if (Worksheet.SelectedImage is SheetImage image)
{
var (from, to) = OffsetAnchors(image.From, image.To, dx, dy);
await ExecuteAsync(new MoveAnchoredCommand<SheetImage>(image, from, to, SpreadsheetFeature.Images));
}
else if (Worksheet.SelectedChart is SheetChart chart)
{
var (from, to) = OffsetAnchors(chart.From, chart.To, dx, dy);
await ExecuteAsync(new MoveAnchoredCommand<SheetChart>(chart, from, to, SpreadsheetFeature.Charts));
}
}
private (CellAnchor From, CellAnchor? To) OffsetAnchors(CellAnchor from, CellAnchor? to, double dx, double dy)
{
var newFrom = from.Clone();
newFrom.ColumnOffset += dx;
newFrom.RowOffset += dy;
NormalizeAnchor(newFrom);
CellAnchor? newTo = null;
if (to is not null)
{
newTo = to.Clone();
newTo.ColumnOffset += dx;
newTo.RowOffset += dy;
NormalizeAnchor(newTo);
}
return (newFrom, newTo);
}
private void AnnounceDrawing()
{
if (Worksheet is null || accessibility is null)
{
return;
}
if (Worksheet.SelectedImage is SheetImage image)
{
accessibility.AnnounceAction(
$"{Localize(nameof(RadzenStrings.Spreadsheet_A11yImageSelected))}, {new CellRef(image.From.Row, image.From.Column)}");
}
else if (Worksheet.SelectedChart is SheetChart chart)
{
accessibility.AnnounceAction(
$"{Localize(nameof(RadzenStrings.Spreadsheet_A11yChartSelected))}, {new CellRef(chart.From.Row, chart.From.Column)}");
}
}
// Keyboard resize: a Size dialog (Shift+F10 in the drawing layer) - the non-drag 2.5.7 alternative.
private async Task OpenDrawingSizeDialogAsync()
{
if (Worksheet is null)
{
return;
}
double width;
double height;
if (Worksheet.SelectedImage is SheetImage selectedImage)
{
width = selectedImage.Width;
height = selectedImage.Height;
}
else if (Worksheet.SelectedChart is SheetChart selectedChart)
{
width = selectedChart.Width;
height = selectedChart.Height;
}
else
{
return;
}
var parameters = new Dictionary<string, object?>
{
{ nameof(Spreadsheet.DrawingSizeDialog.Width), width },
{ nameof(Spreadsheet.DrawingSizeDialog.Height), height },
{ nameof(Spreadsheet.DrawingSizeDialog.WidthLabel), Localize(nameof(RadzenStrings.Spreadsheet_SizeWidth)) },
{ nameof(Spreadsheet.DrawingSizeDialog.HeightLabel), Localize(nameof(RadzenStrings.Spreadsheet_SizeHeight)) },
{ nameof(Spreadsheet.DrawingSizeDialog.OkText), Localize(nameof(RadzenStrings.Spreadsheet_OK)) },
{ nameof(Spreadsheet.DrawingSizeDialog.CancelText), Localize(nameof(RadzenStrings.Spreadsheet_Cancel)) },
};
var result = await OpenDialogAsync<Spreadsheet.DrawingSizeDialog>(
Localize(nameof(RadzenStrings.Spreadsheet_SizeTitle)), parameters, new DialogOptions { Width = "340px" });
if (result is Spreadsheet.DrawingSizeDialog.Size size)
{
if (Worksheet.SelectedImage is SheetImage image)
{
await ExecuteAsync(new ResizeAnchoredCommand<SheetImage>(image, size.Width, size.Height, SpreadsheetFeature.Images));
}
else if (Worksheet.SelectedChart is SheetChart chart)
{
await ExecuteAsync(new ResizeAnchoredCommand<SheetChart>(chart, size.Width, size.Height, SpreadsheetFeature.Charts));
}
}
}
// Announces discrete structural/data actions; the per-cell nav announcer covers cell edits.
private string? DescribeCommand(ICommand command) => command switch
{
SortCommand or MultiKeySortCommand => Localize(nameof(RadzenStrings.Spreadsheet_A11ySorted)),
FilterCommand or TableFilterCommand or SheetAutoFilterCommand or RemoveFilterCommand
=> Localize(nameof(RadzenStrings.Spreadsheet_A11yFilterApplied)),
InsertRowCommand => Localize(nameof(RadzenStrings.Spreadsheet_A11yRowInserted)),
InsertColumnCommand => Localize(nameof(RadzenStrings.Spreadsheet_A11yColumnInserted)),
DeleteRowsCommand => Localize(nameof(RadzenStrings.Spreadsheet_A11yRowDeleted)),
DeleteColumnsCommand => Localize(nameof(RadzenStrings.Spreadsheet_A11yColumnDeleted)),
_ => null
};
private static int IndexOf<T>(IReadOnlyList<T> list, T item) where T : class
{
for (var i = 0; i < list.Count; i++)
{
if (ReferenceEquals(list[i], item))
{
return i;
}
}
return -1;
}
private Task CancelEditAsync()
{
if (Editor?.Mode != EditMode.None)
@@ -999,6 +1454,14 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
}
private readonly Dictionary<string, Func<KeyboardEventArgs, Task>> shortcuts = [];
// Shortcuts that fire regardless of focus context (from the toolbar, formula bar, etc.).
// Everything else in shortcuts is grid-only - dispatched only when focus is in the grid/editor.
private readonly HashSet<string> globalShortcuts = [];
// Approximate page size (rows) for PageUp/PageDown - a screenful without coupling to the viewport.
private const int PageRows = 20;
private readonly SpreadsheetClipboard clipboard = new();
/// <inheritdoc/>
@@ -1018,12 +1481,116 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
shortcuts.Add("Shift+ArrowDown", _ => ExtendSelectionAsync(1, 0));
shortcuts.Add("Shift+ArrowLeft", _ => ExtendSelectionAsync(0, -1));
shortcuts.Add("Shift+ArrowRight", _ => ExtendSelectionAsync(0, 1));
shortcuts.Add("Home", _ => MoveToAsync(c => new CellRef(c.Row, 0)));
shortcuts.Add("End", _ => MoveToAsync(c => new CellRef(c.Row, GetUsedEnd().Column)));
shortcuts.Add("Ctrl+Home", _ => MoveToAsync(_ => new CellRef(0, 0)));
shortcuts.Add("Ctrl+End", _ => MoveToAsync(_ => GetUsedEnd()));
shortcuts.Add("Ctrl+A", _ => SelectUsedRangeAsync());
shortcuts.Add("Shift+Home", _ => ExtendToAsync(c => new CellRef(c.Row, 0)));
shortcuts.Add("Shift+End", _ => ExtendToAsync(c => new CellRef(c.Row, GetUsedEnd().Column)));
shortcuts.Add("Ctrl+Shift+Home", _ => ExtendToAsync(_ => new CellRef(0, 0)));
shortcuts.Add("Ctrl+Shift+End", _ => ExtendToAsync(_ => GetUsedEnd()));
shortcuts.Add("Ctrl+ArrowUp", _ => MoveToAsync(c => FindEdge(c, -1, 0)));
shortcuts.Add("Ctrl+ArrowDown", _ => MoveToAsync(c => FindEdge(c, 1, 0)));
shortcuts.Add("Ctrl+ArrowLeft", _ => MoveToAsync(c => FindEdge(c, 0, -1)));
shortcuts.Add("Ctrl+ArrowRight", _ => MoveToAsync(c => FindEdge(c, 0, 1)));
shortcuts.Add("Ctrl+Shift+ArrowUp", _ => ExtendToAsync(c => FindEdge(c, -1, 0)));
shortcuts.Add("Ctrl+Shift+ArrowDown", _ => ExtendToAsync(c => FindEdge(c, 1, 0)));
shortcuts.Add("Ctrl+Shift+ArrowLeft", _ => ExtendToAsync(c => FindEdge(c, 0, -1)));
shortcuts.Add("Ctrl+Shift+ArrowRight", _ => ExtendToAsync(c => FindEdge(c, 0, 1)));
shortcuts.Add("Ctrl+Space", _ => SelectColumnAsync());
shortcuts.Add("Shift+Space", _ => SelectRowAsync());
shortcuts.Add("Shift+F10", _ => OpenContextMenuAtActiveCellAsync());
shortcuts.Add("ContextMenu", _ => OpenContextMenuAtActiveCellAsync());
shortcuts.Add("Ctrl+Alt+5", _ => EnterDrawingLayerAsync());
shortcuts.Add("PageDown", _ => MoveSelectionAsync(PageRows, 0));
shortcuts.Add("PageUp", _ => MoveSelectionAsync(-PageRows, 0));
shortcuts.Add("Shift+PageDown", _ => ExtendSelectionAsync(PageRows, 0));
shortcuts.Add("Shift+PageUp", _ => ExtendSelectionAsync(-PageRows, 0));
shortcuts.Add("Ctrl+D", _ => FillAsync(down: true));
shortcuts.Add("Ctrl+R", _ => FillAsync(down: false));
shortcuts.Add("Ctrl+C", _ => CopySelectionAsync());
shortcuts.Add("Ctrl+Z", _ => UndoAsync());
shortcuts.Add("Ctrl+X", _ => CutSelectionAsync());
shortcuts.Add("Ctrl+Shift+Z", _ => RedoAsync());
shortcuts.Add("Ctrl+Y", _ => RedoAsync());
shortcuts.Add("Delete", _ => DeleteSelectedAsync());
shortcuts.Add("Backspace", _ => DeleteSelectedAsync());
shortcuts.Add("F2", _ => StartEditActiveCellAsync());
shortcuts.Add("F6", _ => FocusRegionAsync(true));
shortcuts.Add("Shift+F6", _ => FocusRegionAsync(false));
shortcuts.Add("Alt+Slash", _ => OpenShortcutsHelpAsync());
// F6 region escape, the shortcut help and undo/redo work from any focused element; the rest
// are grid-only.
globalShortcuts.Add("F6");
globalShortcuts.Add("Shift+F6");
globalShortcuts.Add("Alt+Slash");
globalShortcuts.Add("Ctrl+Z");
globalShortcuts.Add("Ctrl+Shift+Z");
globalShortcuts.Add("Ctrl+Y");
}
private async Task OpenShortcutsHelpAsync()
{
var rows = new List<Spreadsheet.SpreadsheetShortcutsDialog.Shortcut>
{
new("Arrow keys", Localize(nameof(RadzenStrings.Spreadsheet_HelpMove))),
new("Tab / Shift+Tab", Localize(nameof(RadzenStrings.Spreadsheet_HelpNextCell))),
new("F2", Localize(nameof(RadzenStrings.Spreadsheet_HelpEdit))),
new("F6 / Shift+F6", Localize(nameof(RadzenStrings.Spreadsheet_HelpRegions))),
new("Home / Ctrl+Home", Localize(nameof(RadzenStrings.Spreadsheet_HelpRowStart))),
new("Ctrl+End", Localize(nameof(RadzenStrings.Spreadsheet_HelpLastCell))),
new("Ctrl+A", Localize(nameof(RadzenStrings.Spreadsheet_HelpSelectAll))),
new("Ctrl+C / Ctrl+X / Ctrl+V", Localize(nameof(RadzenStrings.Spreadsheet_HelpClipboard))),
new("Ctrl+Z / Ctrl+Y", Localize(nameof(RadzenStrings.Spreadsheet_HelpUndoRedo))),
new("Delete", Localize(nameof(RadzenStrings.Spreadsheet_HelpClear))),
new("Shift+F10", Localize(nameof(RadzenStrings.Spreadsheet_HelpContextMenu))),
};
var parameters = new Dictionary<string, object?>
{
{ nameof(Spreadsheet.SpreadsheetShortcutsDialog.Shortcuts), rows },
{ nameof(Spreadsheet.SpreadsheetShortcutsDialog.ShortcutColumn), Localize(nameof(RadzenStrings.Spreadsheet_HelpShortcutColumn)) },
{ nameof(Spreadsheet.SpreadsheetShortcutsDialog.ActionColumn), Localize(nameof(RadzenStrings.Spreadsheet_HelpActionColumn)) },
};
await OpenDialogAsync<Spreadsheet.SpreadsheetShortcutsDialog>(
Localize(nameof(RadzenStrings.Spreadsheet_HelpTitle)), parameters, new DialogOptions { Width = "560px" });
}
// Opens a dialog and returns focus to the grid after it closes (WCAG 2.4.3 focus order).
private async Task<dynamic?> OpenDialogAsync<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(
string title, Dictionary<string, object?>? parameters = null, DialogOptions? options = null) where T : ComponentBase
{
var result = await DialogService.OpenAsync<T>(title, parameters, options);
await Element.FocusAsync();
return result;
}
private async Task FocusRegionAsync(bool forward)
{
if (jsRef is not null)
{
await jsRef.InvokeVoidAsync("focusAdjacent", forward);
}
}
private async Task StartEditActiveCellAsync()
{
if (Worksheet is null || Editor is null || !IsFeatureAllowed(SpreadsheetFeature.Editing))
{
return;
}
var address = Worksheet.MergedCells.GetMergedRangeStartOrSelf(Worksheet.Selection.Cell);
var cell = Worksheet.Cells[address];
if (cell != null)
{
await ScrollToAsync(address);
Editor.StartEdit(address, cell.GetValue());
}
}
private async Task DeleteSelectedAsync()
@@ -1465,7 +2032,7 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
if (firstRender && JSRuntime != null)
{
dotNetRef = DotNetObjectReference.Create(this);
jsRef = await JSRuntime.InvokeAsync<IJSObjectReference>("Radzen.createSpreadsheet", Element, dotNetRef, shortcuts.Keys);
jsRef = await JSRuntime.InvokeAsync<IJSObjectReference>("Radzen.createSpreadsheet", Element, dotNetRef, shortcuts.Keys, globalShortcuts);
}
}
@@ -1904,13 +2471,35 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
/// Invoked by JS interop when a key is pressed down.
/// </summary>
[JSInvokable]
public async Task OnKeyDownAsync(KeyboardEventArgs args)
public async Task OnKeyDownAsync(KeyboardEventArgs args, bool isGridContext)
{
ArgumentNullException.ThrowIfNull(args);
if (shortcuts.TryGetValue(TranslateShortcut(args), out var action))
{
await action(args);
// When an image/chart is selected, the grid enters the drawing layer: arrows move it, Tab
// cycles, Esc returns to the grid. Keys the layer doesn't claim fall through to the cell keys.
if (isGridContext && Worksheet is not null &&
(Worksheet.SelectedImage is not null || Worksheet.SelectedChart is not null) &&
await TryHandleDrawingKeyAsync(args))
{
return;
}
var shortcut = TranslateShortcut(args);
if (shortcuts.TryGetValue(shortcut, out var action))
{
// Grid-only shortcuts run only when focus is in the grid or its editor; global ones
// (F6 region escape, undo/redo) run from anywhere so the chrome stays operable.
if (isGridContext || globalShortcuts.Contains(shortcut))
{
await action(args);
}
return;
}
// Type-to-edit only applies inside the grid - never when a toolbar/chrome control has focus.
if (!isGridContext)
{
return;
}
@@ -1924,7 +2513,14 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
return;
}
var ch = args.Key == "Space" ? ' ' : args.Key[0];
// Only single-character keys are printable. This guards IME composition keys (e.g. "Process")
// and named keys like "Home"/"Enter" - the latter previously typed their first letter ("H").
if (args.Key.Length != 1)
{
return;
}
var ch = args.Key[0];
if (char.IsLetterOrDigit(ch) || char.IsPunctuation(ch) || char.IsSymbol(ch) || char.IsSeparator(ch))
{
@@ -2058,20 +2654,42 @@ public partial class RadzenSpreadsheet : RadzenComponent, IAsyncDisposable, ISpr
/// Invoked by JS interop when the pointer is released after resizing a column.
/// </summary>
[JSInvokable]
public Task OnColumnResizePointerUpAsync(PointerEventArgs args)
public async Task OnColumnResizePointerUpAsync(PointerEventArgs args)
{
// The drag set the width live (non-undoable). On release, revert and re-apply via a command
// so the resize joins the undo stack as a single step.
if (activeCapture is ColumnResizeCapture capture && Worksheet != null &&
capture.Column >= 0 && capture.Column < Worksheet.Columns.Count)
{
var finalWidth = Worksheet.Columns[capture.Column];
if (finalWidth != capture.StartWidth)
{
Worksheet.Columns[capture.Column] = capture.StartWidth;
await ExecuteAsync(new ResizeColumnCommand(Worksheet, capture.Column, capture.StartWidth, finalWidth));
}
}
activeCapture = null;
return Task.CompletedTask;
}
/// <summary>
/// Invoked by JS interop when the pointer is released after resizing a row.
/// </summary>
[JSInvokable]
public Task OnRowResizePointerUpAsync(PointerEventArgs args)
public async Task OnRowResizePointerUpAsync(PointerEventArgs args)
{
if (activeCapture is RowResizeCapture capture && Worksheet != null &&
capture.Row >= 0 && capture.Row < Worksheet.Rows.Count)
{
var finalHeight = Worksheet.Rows[capture.Row];
if (finalHeight != capture.StartHeight)
{
Worksheet.Rows[capture.Row] = capture.StartHeight;
await ExecuteAsync(new ResizeRowCommand(Worksheet, capture.Row, capture.StartHeight, finalHeight));
}
}
activeCapture = null;
return Task.CompletedTask;
}
class ColumnResizeCapture

View File

@@ -592,5 +592,72 @@ namespace Radzen.Blazor {
public static string YearView_MoreText { get { return ResourceManager.GetString("YearView_MoreText", resourceCulture); } }
public static string YearView_NoDayEventsText { get { return ResourceManager.GetString("YearView_NoDayEventsText", resourceCulture); } }
public static string DataList_EmptyText { get { return ResourceManager.GetString("DataList_EmptyText", resourceCulture); } }
public static string Spreadsheet_Bold { get { return ResourceManager.GetString("Spreadsheet_Bold", resourceCulture); } }
public static string Spreadsheet_Italic { get { return ResourceManager.GetString("Spreadsheet_Italic", resourceCulture); } }
public static string Spreadsheet_Underline { get { return ResourceManager.GetString("Spreadsheet_Underline", resourceCulture); } }
public static string Spreadsheet_Strikethrough { get { return ResourceManager.GetString("Spreadsheet_Strikethrough", resourceCulture); } }
public static string Spreadsheet_Undo { get { return ResourceManager.GetString("Spreadsheet_Undo", resourceCulture); } }
public static string Spreadsheet_Redo { get { return ResourceManager.GetString("Spreadsheet_Redo", resourceCulture); } }
public static string Spreadsheet_Open { get { return ResourceManager.GetString("Spreadsheet_Open", resourceCulture); } }
public static string Spreadsheet_Save { get { return ResourceManager.GetString("Spreadsheet_Save", resourceCulture); } }
public static string Spreadsheet_TextWrap { get { return ResourceManager.GetString("Spreadsheet_TextWrap", resourceCulture); } }
public static string Spreadsheet_AutoFilter { get { return ResourceManager.GetString("Spreadsheet_AutoFilter", resourceCulture); } }
public static string Spreadsheet_Freeze { get { return ResourceManager.GetString("Spreadsheet_Freeze", resourceCulture); } }
public static string Spreadsheet_InsertRowAfter { get { return ResourceManager.GetString("Spreadsheet_InsertRowAfter", resourceCulture); } }
public static string Spreadsheet_InsertRowBefore { get { return ResourceManager.GetString("Spreadsheet_InsertRowBefore", resourceCulture); } }
public static string Spreadsheet_AlignLeft { get { return ResourceManager.GetString("Spreadsheet_AlignLeft", resourceCulture); } }
public static string Spreadsheet_AlignCenter { get { return ResourceManager.GetString("Spreadsheet_AlignCenter", resourceCulture); } }
public static string Spreadsheet_AlignRight { get { return ResourceManager.GetString("Spreadsheet_AlignRight", resourceCulture); } }
public static string Spreadsheet_AlignTop { get { return ResourceManager.GetString("Spreadsheet_AlignTop", resourceCulture); } }
public static string Spreadsheet_AlignMiddle { get { return ResourceManager.GetString("Spreadsheet_AlignMiddle", resourceCulture); } }
public static string Spreadsheet_AlignBottom { get { return ResourceManager.GetString("Spreadsheet_AlignBottom", resourceCulture); } }
public static string Spreadsheet_FontFamily { get { return ResourceManager.GetString("Spreadsheet_FontFamily", resourceCulture); } }
public static string Spreadsheet_FontSize { get { return ResourceManager.GetString("Spreadsheet_FontSize", resourceCulture); } }
public static string Spreadsheet_DataFormat { get { return ResourceManager.GetString("Spreadsheet_DataFormat", resourceCulture); } }
public static string Spreadsheet_CellBorders { get { return ResourceManager.GetString("Spreadsheet_CellBorders", resourceCulture); } }
public static string Spreadsheet_ConditionalFormatTool { get { return ResourceManager.GetString("Spreadsheet_ConditionalFormatTool", resourceCulture); } }
public static string Spreadsheet_DataValidationTool { get { return ResourceManager.GetString("Spreadsheet_DataValidationTool", resourceCulture); } }
public static string Spreadsheet_A11yGridLabel { get { return ResourceManager.GetString("Spreadsheet_A11yGridLabel", resourceCulture); } }
public static string Spreadsheet_A11yInstructions { get { return ResourceManager.GetString("Spreadsheet_A11yInstructions", resourceCulture); } }
public static string Spreadsheet_A11yBlank { get { return ResourceManager.GetString("Spreadsheet_A11yBlank", resourceCulture); } }
public static string Spreadsheet_A11yMerged { get { return ResourceManager.GetString("Spreadsheet_A11yMerged", resourceCulture); } }
public static string Spreadsheet_A11yPosition { get { return ResourceManager.GetString("Spreadsheet_A11yPosition", resourceCulture); } }
public static string Spreadsheet_A11ySelection { get { return ResourceManager.GetString("Spreadsheet_A11ySelection", resourceCulture); } }
public static string Spreadsheet_A11yFormula { get { return ResourceManager.GetString("Spreadsheet_A11yFormula", resourceCulture); } }
public static string Spreadsheet_A11yError { get { return ResourceManager.GetString("Spreadsheet_A11yError", resourceCulture); } }
public static string Spreadsheet_A11yReadOnly { get { return ResourceManager.GetString("Spreadsheet_A11yReadOnly", resourceCulture); } }
public static string Spreadsheet_A11yValidation { get { return ResourceManager.GetString("Spreadsheet_A11yValidation", resourceCulture); } }
public static string Spreadsheet_A11yHyperlink { get { return ResourceManager.GetString("Spreadsheet_A11yHyperlink", resourceCulture); } }
public static string Spreadsheet_A11yFiltered { get { return ResourceManager.GetString("Spreadsheet_A11yFiltered", resourceCulture); } }
public static string Spreadsheet_A11yConditionalFormat { get { return ResourceManager.GetString("Spreadsheet_A11yConditionalFormat", resourceCulture); } }
public static string Spreadsheet_A11yLeaveHint { get { return ResourceManager.GetString("Spreadsheet_A11yLeaveHint", resourceCulture); } }
public static string Spreadsheet_CellEditorLabel { get { return ResourceManager.GetString("Spreadsheet_CellEditorLabel", resourceCulture); } }
public static string Spreadsheet_FormulaBarLabel { get { return ResourceManager.GetString("Spreadsheet_FormulaBarLabel", resourceCulture); } }
public static string Spreadsheet_HelpTitle { get { return ResourceManager.GetString("Spreadsheet_HelpTitle", resourceCulture); } }
public static string Spreadsheet_HelpShortcutColumn { get { return ResourceManager.GetString("Spreadsheet_HelpShortcutColumn", resourceCulture); } }
public static string Spreadsheet_HelpActionColumn { get { return ResourceManager.GetString("Spreadsheet_HelpActionColumn", resourceCulture); } }
public static string Spreadsheet_HelpMove { get { return ResourceManager.GetString("Spreadsheet_HelpMove", resourceCulture); } }
public static string Spreadsheet_HelpNextCell { get { return ResourceManager.GetString("Spreadsheet_HelpNextCell", resourceCulture); } }
public static string Spreadsheet_HelpEdit { get { return ResourceManager.GetString("Spreadsheet_HelpEdit", resourceCulture); } }
public static string Spreadsheet_HelpRegions { get { return ResourceManager.GetString("Spreadsheet_HelpRegions", resourceCulture); } }
public static string Spreadsheet_HelpRowStart { get { return ResourceManager.GetString("Spreadsheet_HelpRowStart", resourceCulture); } }
public static string Spreadsheet_HelpLastCell { get { return ResourceManager.GetString("Spreadsheet_HelpLastCell", resourceCulture); } }
public static string Spreadsheet_HelpSelectAll { get { return ResourceManager.GetString("Spreadsheet_HelpSelectAll", resourceCulture); } }
public static string Spreadsheet_HelpClipboard { get { return ResourceManager.GetString("Spreadsheet_HelpClipboard", resourceCulture); } }
public static string Spreadsheet_HelpUndoRedo { get { return ResourceManager.GetString("Spreadsheet_HelpUndoRedo", resourceCulture); } }
public static string Spreadsheet_HelpClear { get { return ResourceManager.GetString("Spreadsheet_HelpClear", resourceCulture); } }
public static string Spreadsheet_HelpContextMenu { get { return ResourceManager.GetString("Spreadsheet_HelpContextMenu", resourceCulture); } }
public static string Spreadsheet_A11yImageSelected { get { return ResourceManager.GetString("Spreadsheet_A11yImageSelected", resourceCulture); } }
public static string Spreadsheet_A11yChartSelected { get { return ResourceManager.GetString("Spreadsheet_A11yChartSelected", resourceCulture); } }
public static string Spreadsheet_SizeTitle { get { return ResourceManager.GetString("Spreadsheet_SizeTitle", resourceCulture); } }
public static string Spreadsheet_SizeWidth { get { return ResourceManager.GetString("Spreadsheet_SizeWidth", resourceCulture); } }
public static string Spreadsheet_SizeHeight { get { return ResourceManager.GetString("Spreadsheet_SizeHeight", resourceCulture); } }
public static string Spreadsheet_SizeApply { get { return ResourceManager.GetString("Spreadsheet_SizeApply", resourceCulture); } }
public static string Spreadsheet_A11ySorted { get { return ResourceManager.GetString("Spreadsheet_A11ySorted", resourceCulture); } }
public static string Spreadsheet_A11yFilterApplied { get { return ResourceManager.GetString("Spreadsheet_A11yFilterApplied", resourceCulture); } }
public static string Spreadsheet_A11yRowInserted { get { return ResourceManager.GetString("Spreadsheet_A11yRowInserted", resourceCulture); } }
public static string Spreadsheet_A11yColumnInserted { get { return ResourceManager.GetString("Spreadsheet_A11yColumnInserted", resourceCulture); } }
public static string Spreadsheet_A11yRowDeleted { get { return ResourceManager.GetString("Spreadsheet_A11yRowDeleted", resourceCulture); } }
public static string Spreadsheet_A11yColumnDeleted { get { return ResourceManager.GetString("Spreadsheet_A11yColumnDeleted", resourceCulture); } }
}
}

View File

@@ -1743,4 +1743,205 @@
<data name="DataList_EmptyText" xml:space="preserve">
<value>No records to display.</value>
</data>
<data name="Spreadsheet_Bold" xml:space="preserve">
<value>Bold</value>
</data>
<data name="Spreadsheet_Italic" xml:space="preserve">
<value>Italic</value>
</data>
<data name="Spreadsheet_Underline" xml:space="preserve">
<value>Underline</value>
</data>
<data name="Spreadsheet_Strikethrough" xml:space="preserve">
<value>Strikethrough</value>
</data>
<data name="Spreadsheet_Undo" xml:space="preserve">
<value>Undo</value>
</data>
<data name="Spreadsheet_Redo" xml:space="preserve">
<value>Redo</value>
</data>
<data name="Spreadsheet_Open" xml:space="preserve">
<value>Open</value>
</data>
<data name="Spreadsheet_Save" xml:space="preserve">
<value>Save</value>
</data>
<data name="Spreadsheet_TextWrap" xml:space="preserve">
<value>Wrap text</value>
</data>
<data name="Spreadsheet_AutoFilter" xml:space="preserve">
<value>Filter</value>
</data>
<data name="Spreadsheet_Freeze" xml:space="preserve">
<value>Freeze panes</value>
</data>
<data name="Spreadsheet_InsertRowAfter" xml:space="preserve">
<value>Insert row below</value>
</data>
<data name="Spreadsheet_InsertRowBefore" xml:space="preserve">
<value>Insert row above</value>
</data>
<data name="Spreadsheet_AlignLeft" xml:space="preserve">
<value>Align left</value>
</data>
<data name="Spreadsheet_AlignCenter" xml:space="preserve">
<value>Align center</value>
</data>
<data name="Spreadsheet_AlignRight" xml:space="preserve">
<value>Align right</value>
</data>
<data name="Spreadsheet_AlignTop" xml:space="preserve">
<value>Align top</value>
</data>
<data name="Spreadsheet_AlignMiddle" xml:space="preserve">
<value>Align middle</value>
</data>
<data name="Spreadsheet_AlignBottom" xml:space="preserve">
<value>Align bottom</value>
</data>
<data name="Spreadsheet_FontFamily" xml:space="preserve">
<value>Font</value>
</data>
<data name="Spreadsheet_FontSize" xml:space="preserve">
<value>Font size</value>
</data>
<data name="Spreadsheet_DataFormat" xml:space="preserve">
<value>Number format</value>
</data>
<data name="Spreadsheet_CellBorders" xml:space="preserve">
<value>Borders</value>
</data>
<data name="Spreadsheet_ConditionalFormatTool" xml:space="preserve">
<value>Conditional formatting</value>
</data>
<data name="Spreadsheet_DataValidationTool" xml:space="preserve">
<value>Data validation</value>
</data>
<data name="Spreadsheet_A11yGridLabel" xml:space="preserve">
<value>Spreadsheet</value>
</data>
<data name="Spreadsheet_A11yInstructions" xml:space="preserve">
<value>Arrow keys move between cells. Tab moves to the next cell. F6 moves to the toolbar and out. F2 or type to edit. Press Alt+Slash for all keyboard shortcuts.</value>
</data>
<data name="Spreadsheet_A11yBlank" xml:space="preserve">
<value>blank</value>
</data>
<data name="Spreadsheet_A11yMerged" xml:space="preserve">
<value>merged</value>
</data>
<data name="Spreadsheet_A11yPosition" xml:space="preserve">
<value>row {0} of {1}, column {2} of {3}</value>
</data>
<data name="Spreadsheet_A11ySelection" xml:space="preserve">
<value>{0}, {1} by {2}</value>
</data>
<data name="Spreadsheet_A11yFormula" xml:space="preserve">
<value>has formula</value>
</data>
<data name="Spreadsheet_A11yError" xml:space="preserve">
<value>error</value>
</data>
<data name="Spreadsheet_A11yReadOnly" xml:space="preserve">
<value>locked</value>
</data>
<data name="Spreadsheet_A11yValidation" xml:space="preserve">
<value>data validation</value>
</data>
<data name="Spreadsheet_A11yHyperlink" xml:space="preserve">
<value>hyperlink</value>
</data>
<data name="Spreadsheet_A11yFiltered" xml:space="preserve">
<value>filtered</value>
</data>
<data name="Spreadsheet_A11yConditionalFormat" xml:space="preserve">
<value>conditional formatting</value>
</data>
<data name="Spreadsheet_A11yLeaveHint" xml:space="preserve">
<value>Press F6 to leave the grid</value>
</data>
<data name="Spreadsheet_CellEditorLabel" xml:space="preserve">
<value>Cell editor</value>
</data>
<data name="Spreadsheet_FormulaBarLabel" xml:space="preserve">
<value>Formula bar</value>
</data>
<data name="Spreadsheet_HelpTitle" xml:space="preserve">
<value>Keyboard shortcuts</value>
</data>
<data name="Spreadsheet_HelpShortcutColumn" xml:space="preserve">
<value>Shortcut</value>
</data>
<data name="Spreadsheet_HelpActionColumn" xml:space="preserve">
<value>Action</value>
</data>
<data name="Spreadsheet_HelpMove" xml:space="preserve">
<value>Move between cells</value>
</data>
<data name="Spreadsheet_HelpNextCell" xml:space="preserve">
<value>Move to the next or previous cell</value>
</data>
<data name="Spreadsheet_HelpEdit" xml:space="preserve">
<value>Edit the active cell</value>
</data>
<data name="Spreadsheet_HelpRegions" xml:space="preserve">
<value>Move between the toolbar, formula bar, grid and sheet tabs</value>
</data>
<data name="Spreadsheet_HelpRowStart" xml:space="preserve">
<value>Go to the start of the row, or to cell A1</value>
</data>
<data name="Spreadsheet_HelpLastCell" xml:space="preserve">
<value>Go to the last used cell</value>
</data>
<data name="Spreadsheet_HelpSelectAll" xml:space="preserve">
<value>Select the used range</value>
</data>
<data name="Spreadsheet_HelpClipboard" xml:space="preserve">
<value>Copy, cut and paste</value>
</data>
<data name="Spreadsheet_HelpUndoRedo" xml:space="preserve">
<value>Undo and redo</value>
</data>
<data name="Spreadsheet_HelpClear" xml:space="preserve">
<value>Clear the selected cells</value>
</data>
<data name="Spreadsheet_HelpContextMenu" xml:space="preserve">
<value>Open the context menu</value>
</data>
<data name="Spreadsheet_A11yImageSelected" xml:space="preserve">
<value>Image selected</value>
</data>
<data name="Spreadsheet_A11yChartSelected" xml:space="preserve">
<value>Chart selected</value>
</data>
<data name="Spreadsheet_SizeTitle" xml:space="preserve">
<value>Size and position</value>
</data>
<data name="Spreadsheet_SizeWidth" xml:space="preserve">
<value>Width (px)</value>
</data>
<data name="Spreadsheet_SizeHeight" xml:space="preserve">
<value>Height (px)</value>
</data>
<data name="Spreadsheet_SizeApply" xml:space="preserve">
<value>Apply</value>
</data>
<data name="Spreadsheet_A11ySorted" xml:space="preserve">
<value>Sorted</value>
</data>
<data name="Spreadsheet_A11yFilterApplied" xml:space="preserve">
<value>Filter applied</value>
</data>
<data name="Spreadsheet_A11yRowInserted" xml:space="preserve">
<value>Row inserted</value>
</data>
<data name="Spreadsheet_A11yColumnInserted" xml:space="preserve">
<value>Column inserted</value>
</data>
<data name="Spreadsheet_A11yRowDeleted" xml:space="preserve">
<value>Row deleted</value>
</data>
<data name="Spreadsheet_A11yColumnDeleted" xml:space="preserve">
<value>Column deleted</value>
</data>
</root>

View File

@@ -18,7 +18,8 @@
else
{
<SheetEditor @bind-Value=@Editor.Value AutoFocus=@(Editor.Mode == EditMode.Cell) Blur=@(this.AsNonRenderingEventHandler(OnBlurAsync))
Worksheet=@Worksheet Focus=@(this.AsNonRenderingEventHandler(OnFocus)) style=@editorStyle />
Worksheet=@Worksheet Focus=@(this.AsNonRenderingEventHandler(OnFocus)) style=@editorStyle
InputAttributes=@(new Dictionary<string, object> { { "aria-label", Spreadsheet.Localize(nameof(RadzenStrings.Spreadsheet_CellEditorLabel)) } }) />
}
</div>
}

View File

@@ -0,0 +1,54 @@
@namespace Radzen.Blazor.Spreadsheet
@using Radzen.Blazor
@inject DialogService DialogService
@* Keyboard resize for a selected image/chart - the non-drag pointer alternative (WCAG 2.5.7).
The host supplies the current size and localized labels; OK returns the new Size. *@
<RadzenStack Gap="1rem">
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0.5rem">
<RadzenLabel Text=@WidthLabel Component="rz-drawing-width" style="min-width:6rem" />
<RadzenNumeric @bind-Value=@width TValue="double" Min="1" Name="rz-drawing-width" aria-label=@WidthLabel />
</RadzenStack>
<RadzenStack Orientation="Orientation.Horizontal" AlignItems="AlignItems.Center" Gap="0.5rem">
<RadzenLabel Text=@HeightLabel Component="rz-drawing-height" style="min-width:6rem" />
<RadzenNumeric @bind-Value=@height TValue="double" Min="1" Name="rz-drawing-height" aria-label=@HeightLabel />
</RadzenStack>
<RadzenStack Orientation="Orientation.Horizontal" JustifyContent="JustifyContent.End" Gap="0.5rem">
<RadzenButton Text=@CancelText Click=@(() => DialogService.Close(null)) Variant="Variant.Flat" ButtonStyle="ButtonStyle.Light" />
<RadzenButton Text=@OkText Click=@OnOk ButtonStyle="ButtonStyle.Primary" />
</RadzenStack>
</RadzenStack>
@code {
/// <summary>Represents the chosen drawing size in pixels.</summary>
public sealed record Size(double Width, double Height);
/// <summary>The current width in pixels.</summary>
[Parameter] public double Width { get; set; }
/// <summary>The current height in pixels.</summary>
[Parameter] public double Height { get; set; }
/// <summary>Localized label for the width input.</summary>
[Parameter] public string? WidthLabel { get; set; }
/// <summary>Localized label for the height input.</summary>
[Parameter] public string? HeightLabel { get; set; }
/// <summary>Localized confirm-button text.</summary>
[Parameter] public string? OkText { get; set; }
/// <summary>Localized cancel-button text.</summary>
[Parameter] public string? CancelText { get; set; }
private double width;
private double height;
protected override void OnInitialized()
{
width = Width;
height = Height;
}
private void OnOk() => DialogService.Close(new Size(width, height));
}

View File

@@ -4,7 +4,8 @@
@using Radzen.Documents.Spreadsheet
<div class="rz-spreadsheet-formula-editor">
<SheetEditor tabindex="-1" @bind-Value=@Editor.Value Focus=@(this.AsNonRenderingEventHandler(OnFocus))
<SheetEditor @bind-Value=@Editor.Value Focus=@(this.AsNonRenderingEventHandler(OnFocus))
Worksheet=@Worksheet Blur=@(this.AsNonRenderingEventHandler(OnBlurAsync))
ReadOnly=@(Spreadsheet?.IsFeatureAllowed(SpreadsheetFeature.Editing) == false) />
ReadOnly=@(Spreadsheet?.IsFeatureAllowed(SpreadsheetFeature.Editing) == false)
InputAttributes=@(new Dictionary<string, object> { { "aria-label", (object)(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FormulaBarLabel)) ?? "Formula bar") }, { "tabindex", "-1" } }) />
</div>

View File

@@ -0,0 +1,47 @@
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
/// <summary>
/// Undoable command that resizes a column to a new width.
/// </summary>
public sealed class ResizeColumnCommand(Worksheet sheet, int column, double oldWidth, double newWidth) : ICommand
{
/// <inheritdoc/>
public SpreadsheetFeature? Feature => null;
/// <inheritdoc/>
public bool Execute()
{
sheet.Columns[column] = newWidth;
return true;
}
/// <inheritdoc/>
public void Unexecute()
{
sheet.Columns[column] = oldWidth;
}
}
/// <summary>
/// Undoable command that resizes a row to a new height.
/// </summary>
public sealed class ResizeRowCommand(Worksheet sheet, int row, double oldHeight, double newHeight) : ICommand
{
/// <inheritdoc/>
public SpreadsheetFeature? Feature => null;
/// <inheritdoc/>
public bool Execute()
{
sheet.Rows[row] = newHeight;
return true;
}
/// <inheritdoc/>
public void Unexecute()
{
sheet.Rows[row] = oldHeight;
}
}

View File

@@ -3,7 +3,7 @@
@using Radzen.Blazor.Rendering
@inject IJSRuntime JSRuntime;
<div class="rz-spreadsheet-editor" @attributes=@Attributes>
<div class="rz-spreadsheet-editor-input" spellcheck="false" @ref=@element contenteditable=@(ReadOnly ? "false" : "true")></div>
<div class="rz-spreadsheet-editor-input" spellcheck="false" @ref=@element role="textbox" aria-multiline="false" contenteditable=@(ReadOnly ? "false" : "true") @attributes=@InputAttributes></div>
<SheetEditorHighlight Value=@value />
</div>
<RadzenPopup @ref=@popup class="rz-spreadsheet-highlight-popup rz-input-sm" Close="OnPopupClose">

View File

@@ -67,6 +67,14 @@ public partial class SheetEditor : ComponentBase, IAsyncDisposable
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? Attributes { get; set; }
/// <summary>
/// Gets or sets HTML attributes applied to the inner contenteditable (the focusable text field):
/// e.g. <c>aria-label</c>, <c>aria-invalid</c>, <c>tabindex</c>. Unlike <see cref="Attributes"/>
/// (which lands on the outer wrapper), these reach the element the screen reader names and focuses.
/// </summary>
[Parameter]
public IReadOnlyDictionary<string, object>? InputAttributes { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the content editable element should automatically receive focus when rendered.
/// </summary>

View File

@@ -0,0 +1,8 @@
@namespace Radzen.Blazor.Spreadsheet
@using Radzen.Blazor
@* Three live regions, present (empty) from first render. nav = cell navigation, action = transient
action results, alert = assertive validation/error messages. *@
<RadzenLiveRegion Text=@navText />
<RadzenLiveRegion Text=@actionText />
<RadzenLiveRegion Text=@alertText Assertive="true" />

View File

@@ -0,0 +1,219 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Microsoft.AspNetCore.Components;
using Radzen.Documents.Spreadsheet;
namespace Radzen.Blazor.Spreadsheet;
#nullable enable
/// <summary>
/// Isolated, perf-safe screen-reader announcer for <see cref="RadzenSpreadsheet"/>. It binds to the
/// worksheet <see cref="Selection"/> and, on selection change, re-renders ONLY itself (three visually
/// hidden <c>aria-live</c> regions) - the virtualized cell grid is never touched, so navigation stays
/// on the existing perf-neutral hot path.
/// </summary>
public partial class SpreadsheetAccessibility : ComponentBase, IDisposable
{
/// <summary>The worksheet whose selection is announced.</summary>
[Parameter]
public Worksheet Worksheet { get; set; } = default!;
/// <summary>The host spreadsheet, used to localize announcement templates.</summary>
[Parameter]
public ISpreadsheet Spreadsheet { get; set; } = default!;
private string? navText;
private string? actionText;
private string? alertText;
private string? lastNav;
private string? lastRange;
private readonly EventBinding<Selection> selectionBinding;
/// <summary>
/// Initializes a new instance of the <see cref="SpreadsheetAccessibility"/> class.
/// </summary>
public SpreadsheetAccessibility()
{
selectionBinding = new EventBinding<Selection>(
s => s.Changed += OnSelectionChanged,
s => s.Changed -= OnSelectionChanged);
}
/// <inheritdoc/>
protected override void OnParametersSet()
{
selectionBinding.Bind(Worksheet?.Selection);
}
private void OnSelectionChanged()
{
if (Worksheet is null)
{
return;
}
var changed = false;
// Active cell (polite nav region) - skip identical consecutive announcements.
var nav = BuildAnnouncement(Worksheet, Worksheet.Selection.Cell, Localize);
if (nav != lastNav)
{
lastNav = nav;
navText = nav;
changed = true;
}
// Selection size (separate action region) when a multi-cell range is selected, e.g. "B3:D6, 4 by 3".
var range = Worksheet.Selection.Range;
string? rangeText = null;
if (!range.Collapsed)
{
rangeText = string.Format(CultureInfo.CurrentCulture,
Localize(nameof(RadzenStrings.Spreadsheet_A11ySelection)),
$"{range.Start}:{range.End}", range.Rows, range.Columns);
}
if (rangeText != lastRange)
{
lastRange = rangeText;
actionText = rangeText;
changed = true;
}
if (changed)
{
StateHasChanged();
}
}
/// <summary>
/// Announces a transient action result (sort, filter, insert, undo) in a polite region, keeping
/// the active cell named so context is not lost.
/// </summary>
public void AnnounceAction(string message)
{
actionText = message;
StateHasChanged();
}
/// <summary>Announces an assertive alert (for example a validation rejection).</summary>
public void AnnounceAlert(string message)
{
alertText = message;
StateHasChanged();
}
private string Localize(string key) => Spreadsheet?.Localize(key) ?? key;
/// <summary>
/// Composes the spoken description of a cell - address, content (or "blank"), header, position,
/// merged span and state - from the model. Pure and deterministic for unit testing.
/// </summary>
/// <param name="worksheet">The worksheet that owns the cell.</param>
/// <param name="cell">The active cell reference.</param>
/// <param name="localize">Resolves a <see cref="RadzenStrings"/> key to localized text.</param>
public static string BuildAnnouncement(Worksheet worksheet, CellRef cell, Func<string, string> localize)
{
ArgumentNullException.ThrowIfNull(localize);
if (worksheet is null || cell == CellRef.Invalid)
{
return string.Empty;
}
var parts = new List<string>();
// Address first ("B3").
parts.Add(cell.ToString());
// Content (formatted) or "blank".
worksheet.Cells.TryGet(cell.Row, cell.Column, out var c);
var content = GetCellText(c);
parts.Add(string.IsNullOrEmpty(content)
? localize(nameof(RadzenStrings.Spreadsheet_A11yBlank))
: content);
// Header from a frozen header row (suppressed when there is none or it would echo the content).
var header = GetHeader(worksheet, cell, content);
if (!string.IsNullOrEmpty(header))
{
parts.Add(header);
}
// Position: "row R of N, column C of M".
parts.Add(string.Format(CultureInfo.CurrentCulture, localize(nameof(RadzenStrings.Spreadsheet_A11yPosition)),
cell.Row + 1, worksheet.RowCount, ColumnRef.ToString(cell.Column), worksheet.ColumnCount));
// Merged span.
if (worksheet.MergedCells.Contains(cell))
{
parts.Add(localize(nameof(RadzenStrings.Spreadsheet_A11yMerged)));
}
// Cell state (all read from the model, no fabricated meaning).
if (c is not null)
{
if (c.Formula is not null)
{
parts.Add(localize(nameof(RadzenStrings.Spreadsheet_A11yFormula)));
}
if (c.ValueType == CellDataType.Error)
{
parts.Add(localize(nameof(RadzenStrings.Spreadsheet_A11yError)));
}
if (c.Hyperlink is not null)
{
parts.Add(localize(nameof(RadzenStrings.Spreadsheet_A11yHyperlink)));
}
if (worksheet.Protection.IsProtected && c.Format.IsLocked)
{
parts.Add(localize(nameof(RadzenStrings.Spreadsheet_A11yReadOnly)));
}
}
if (worksheet.FilteredColumns.Contains(cell.Column))
{
parts.Add(localize(nameof(RadzenStrings.Spreadsheet_A11yFiltered)));
}
return string.Join(", ", parts);
}
private static string GetCellText(Cell? c)
{
if (c is null || c.IsEmpty)
{
return string.Empty;
}
return NumberFormat.Apply(c.Format.NumberFormat, c.Value, c.ValueType)
?? c.Value?.ToString()
?? string.Empty;
}
private static string GetHeader(Worksheet worksheet, CellRef cell, string content)
{
var frozen = worksheet.Rows.Frozen;
if (frozen <= 0 || cell.Row < frozen)
{
return string.Empty;
}
if (worksheet.Cells.TryGet(frozen - 1, cell.Column, out var hc))
{
var ht = GetCellText(hc);
if (!string.IsNullOrEmpty(ht) && ht != content)
{
return ht;
}
}
return string.Empty;
}
void IDisposable.Dispose()
{
selectionBinding.Dispose();
}
}

View File

@@ -0,0 +1,39 @@
@namespace Radzen.Blazor.Spreadsheet
@using Radzen.Blazor
@* A read-only reference of keyboard shortcuts, opened from the grid via Alt+/. The rows are built
and localized by the host (RadzenSpreadsheet) and passed in, so this dialog needs no cascade. *@
<RadzenTable class="rz-spreadsheet-shortcuts">
<RadzenTableHeader>
<RadzenTableHeaderRow>
<RadzenTableHeaderCell>@ShortcutColumn</RadzenTableHeaderCell>
<RadzenTableHeaderCell>@ActionColumn</RadzenTableHeaderCell>
</RadzenTableHeaderRow>
</RadzenTableHeader>
<RadzenTableBody>
@foreach (var shortcut in Shortcuts)
{
<RadzenTableRow>
<RadzenTableCell style="white-space:nowrap"><kbd>@shortcut.Keys</kbd></RadzenTableCell>
<RadzenTableCell>@shortcut.Description</RadzenTableCell>
</RadzenTableRow>
}
</RadzenTableBody>
</RadzenTable>
@code {
/// <summary>Represents one keyboard shortcut row: the key combination and what it does.</summary>
public sealed record Shortcut(string Keys, string Description);
/// <summary>The localized shortcut rows to display, supplied by the host.</summary>
[Parameter]
public IReadOnlyList<Shortcut> Shortcuts { get; set; } = [];
/// <summary>Localized "Shortcut" column header.</summary>
[Parameter]
public string? ShortcutColumn { get; set; }
/// <summary>Localized "Action" column header.</summary>
[Parameter]
public string? ActionColumn { get; set; }
}

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenToggleButton Value=@IsAutoFilterEnabled ValueChanged=@OnToggleAutoFilterAsync Icon="filter_list" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenToggleButton AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AutoFilter))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AutoFilter))) Value=@IsAutoFilterEnabled ValueChanged=@OnToggleAutoFilterAsync Icon="filter_list" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
ToggleVariant="Variant.Flat" ToggleButtonStyle="ButtonStyle.Primary" ToggleShade="Shade.Lighter" />
@code {

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenColorPicker Value=@CurrentColor ValueChanged=@OnColorChangedAsync Icon="opacity" Disabled=@IsDisabled InputSize="InputSize.Small" ShowArrow="false" />
<RadzenColorPicker ToggleAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_BackgroundColor))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_BackgroundColor))) Value=@CurrentColor ValueChanged=@OnColorChangedAsync Icon="opacity" Disabled=@IsDisabled InputSize="InputSize.Small" ShowArrow="false" />
@code {
#nullable enable

View File

@@ -2,7 +2,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetFormatToggleToolBase
<RadzenToggleButton Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="format_bold" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenToggleButton AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Bold))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Bold))) Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="format_bold" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
ToggleVariant="Variant.Flat" ToggleButtonStyle="ButtonStyle.Primary" ToggleShade="Shade.Lighter" />
@code {

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenSplitButton Click=@OnClick Icon="border_all" Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_CellBorders))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_CellBorders))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_CellBorders))) Click=@OnClick Icon="border_all" Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<ChildContent>
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AllBorders)) ?? "All Borders") Value="all" />
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_OutsideBorders)) ?? "Outside Borders") Value="outside" />

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenColorPicker Value=@CurrentColor ValueChanged=@OnColorChangedAsync Icon="title" Disabled=@IsDisabled InputSize="InputSize.Small" ShowArrow="false" />
<RadzenColorPicker ToggleAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_TextColor))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_TextColor))) Value=@CurrentColor ValueChanged=@OnColorChangedAsync Icon="title" Disabled=@IsDisabled InputSize="InputSize.Small" ShowArrow="false" />
@code {
#nullable enable

View File

@@ -6,7 +6,7 @@
@inject DialogService DialogService
@inherits RadzenSpreadsheetToolBase
<RadzenSplitButton Click=@OnClickAsync Icon="format_color_fill" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Conditional)) ?? "Conditional") Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_ConditionalFormatTool))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_ConditionalFormatTool))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_ConditionalFormatTool))) Click=@OnClickAsync Icon="format_color_fill" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Conditional)) ?? "Conditional") Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<ChildContent>
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_GreaterThanMenu)) ?? "Greater Than...") Value="greater" />
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_LessThanMenu)) ?? "Less Than...") Value="less" />

View File

@@ -4,7 +4,7 @@
@inherits RadzenSpreadsheetToolBase
@inject DialogService DialogService
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" ButtonType="ButtonType.Button"
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_CustomSort))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" ButtonType="ButtonType.Button"
Disabled=@IsDisabled
Click=@OnClick
Icon="sort"

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenDropDown Value=@CurrentFormat ValueChanged=@OnFormatChangedAsync TValue="string" Data=@FormatOptions TextProperty="Name" ValueProperty="FormatCode"
<RadzenDropDown EmptyAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DataFormat))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DataFormat))) Value=@CurrentFormat ValueChanged=@OnFormatChangedAsync TValue="string" Data=@FormatOptions TextProperty="Name" ValueProperty="FormatCode"
Disabled=@IsDisabled InputSize="InputSize.Small" Style="width: 130px" />
@code {

View File

@@ -6,7 +6,7 @@
@inject DialogService DialogService
@inherits RadzenSpreadsheetToolBase
<RadzenSplitButton Click=@OnClickAsync Icon="rule" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Validation)) ?? "Validation") Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DataValidationTool))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DataValidationTool))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DataValidationTool))) Click=@OnClickAsync Icon="rule" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Validation)) ?? "Validation") Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<ChildContent>
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DataValidationMenu)) ?? "Data Validation...") Value="validate" />
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_ClearValidation)) ?? "Clear Validation") Value="clear" />

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenButton Text=".0-" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnClick title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DecreaseDecimalPlaces)) ?? "Decrease decimal places") />
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DecreaseDecimalPlaces))) Text=".0-" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnClick title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_DecreaseDecimalPlaces)) ?? "Decrease decimal places") />
@code {
#nullable enable

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenDropDown Value=@CurrentFontFamily ValueChanged=@OnFontFamilyChangedAsync TValue="string" Data=@FontFamilies
<RadzenDropDown EmptyAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FontFamily))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FontFamily))) Value=@CurrentFontFamily ValueChanged=@OnFontFamilyChangedAsync TValue="string" Data=@FontFamilies
Disabled=@IsDisabled InputSize="InputSize.Small" Style="width: 140px" />
@code {

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenDropDown Value=@CurrentFontSize ValueChanged=@OnFontSizeChangedAsync TValue="double" Data=@FontSizes
<RadzenDropDown EmptyAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FontSize))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FontSize))) Value=@CurrentFontSize ValueChanged=@OnFontSizeChangedAsync TValue="double" Data=@FontSizes
Disabled=@IsDisabled InputSize="InputSize.Small" Style="width: 65px" />
@code {

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenSplitButton Icon="mode_cool" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FreezePanes)) ?? "Freeze panes") Click=@OnFreezeClick Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Disabled=@IsDisabled>
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Freeze))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Freeze))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Freeze))) Icon="mode_cool" Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FreezePanes)) ?? "Freeze panes") Click=@OnFreezeClick Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Disabled=@IsDisabled>
<ChildContent>
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FreezeTopRow)) ?? "Freeze Top Row") Value="row" Icon="splitscreen_top" />
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_FreezeSelectedRows)) ?? "Freeze Selected Rows") Value="rows" Icon="split_scene_up" />

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenButton Text=".0+" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnClick title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_IncreaseDecimalPlaces)) ?? "Increase decimal places") />
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_IncreaseDecimalPlaces))) Text=".0+" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnClick title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_IncreaseDecimalPlaces)) ?? "Increase decimal places") />
@code {
#nullable enable

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenSplitButton Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertChart))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertChart))) Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
Disabled=@IsDisabled Click=@OnClick
Icon="bar_chart" Text="Chart"
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertChart)) ?? "Insert Chart")>

View File

@@ -2,7 +2,7 @@
@using Radzen.Blazor.Spreadsheet
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertColumnAfterClick Icon="add_column_right" ButtonType="ButtonType.Button" Disabled=@IsDisabled
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertColumnAfter))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertColumnAfterClick Icon="add_column_right" ButtonType="ButtonType.Button" Disabled=@IsDisabled
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertColumnAfter)) ?? "Insert Column After") />
@code {
protected override SpreadsheetFeature? Feature => SpreadsheetFeature.Editing;

View File

@@ -2,7 +2,7 @@
@using Radzen.Blazor.Spreadsheet
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertColumnBeforeClick Icon="add_column_left" ButtonType="ButtonType.Button" Disabled=@IsDisabled
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertColumnBefore))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertColumnBeforeClick Icon="add_column_left" ButtonType="ButtonType.Button" Disabled=@IsDisabled
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertColumnBefore)) ?? "Insert Column Before") />
@code {
protected override SpreadsheetFeature? Feature => SpreadsheetFeature.Editing;

View File

@@ -6,7 +6,7 @@
@inherits RadzenSpreadsheetToolBase
@inject DialogService DialogService
<RadzenButton Icon="link" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Click=@OnClickAsync Size="ButtonSize.Small"
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertHyperlink))) Icon="link" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Click=@OnClickAsync Size="ButtonSize.Small"
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertHyperlink)) ?? "Insert Hyperlink") />
@code {

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" class="rz-fileupload-choose rz-button-icon-only" ButtonType="ButtonType.Button"
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertImage))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" class="rz-fileupload-choose rz-button-icon-only" ButtonType="ButtonType.Button"
Disabled=@IsDisabled Size="ButtonSize.Small"
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertImage)) ?? "Insert Image")>
<InputFile OnChange=@OnFileSelectedAsync accept="image/*" disabled=@IsDisabled />

View File

@@ -2,7 +2,7 @@
@using Radzen.Blazor.Spreadsheet
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertRowAfterClick Icon="add_row_below" ButtonType="ButtonType.Button" Disabled=@IsDisabled
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertRowBelow))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertRowAfterClick Icon="add_row_below" ButtonType="ButtonType.Button" Disabled=@IsDisabled
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertRowBelow)) ?? "Insert Row Below") />
@code {
protected override SpreadsheetFeature? Feature => SpreadsheetFeature.Editing;

View File

@@ -2,7 +2,7 @@
@using Radzen.Blazor.Spreadsheet
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertRowBeforeClick Icon="add_row_above" ButtonType="ButtonType.Button" Disabled=@IsDisabled
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertRowAbove))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnInsertRowBeforeClick Icon="add_row_above" ButtonType="ButtonType.Button" Disabled=@IsDisabled
title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertRowAbove)) ?? "Insert Row Above") />
@code {
protected override SpreadsheetFeature? Feature => SpreadsheetFeature.Editing;

View File

@@ -4,7 +4,7 @@
@inherits RadzenSpreadsheetToolBase
@inject DialogService DialogService
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" ButtonType="ButtonType.Button"
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_InsertTable))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" ButtonType="ButtonType.Button"
Disabled=@IsDisabled
Click=@OnClick
Icon="table_chart"

View File

@@ -2,7 +2,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetFormatToggleToolBase
<RadzenToggleButton Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="format_italic" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenToggleButton AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Italic))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Italic))) Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="format_italic" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
ToggleVariant="Variant.Flat" ToggleButtonStyle="ButtonStyle.Primary" ToggleShade="Shade.Lighter" />
@code {

View File

@@ -4,7 +4,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetToolBase
<RadzenSplitButton Click=@OnClick Icon="merge_type" Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_MergeCells))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_MergeCells))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_MergeCells))) Click=@OnClick Icon="merge_type" Disabled=@IsDisabled Variant="Variant.Outlined" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small">
<ChildContent>
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_MergeAndCenter)) ?? "Merge && Center") Value="merge-center" />
<RadzenSplitButtonItem Text=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_MergeCells)) ?? "Merge Cells") Value="merge" />

View File

@@ -3,7 +3,7 @@
@using Microsoft.AspNetCore.Components.Forms
@using System.IO
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" class="rz-fileupload-choose rz-button-icon-only" ButtonType="ButtonType.Button">
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Open))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Open))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" class="rz-fileupload-choose rz-button-icon-only" ButtonType="ButtonType.Button">
<InputFile OnChange=@OnUploadAsync accept=".xlsx,.csv,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv" />
<i class="notranslate rzi">folder_open</i>
</RadzenButton>

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnRedoAsync Icon="redo" ButtonType="ButtonType.Button" Disabled=@IsDisabled />
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Redo))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Redo))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnRedoAsync Icon="redo" ButtonType="ButtonType.Button" Disabled=@IsDisabled />
@code {
#nullable enable

View File

@@ -4,7 +4,7 @@
@using System.IO
@inject IJSRuntime JSRuntime
<RadzenSplitButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Icon="download" ButtonType="ButtonType.Button" Size="ButtonSize.Small"
<RadzenSplitButton ButtonAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Save))) OpenAriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Save))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Save))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Icon="download" ButtonType="ButtonType.Button" Size="ButtonSize.Small"
Disabled=@(Workbook is null) Click=@OnClickAsync>
<ChildContent>
<RadzenSplitButtonItem Text="Excel (.xlsx)" Icon="table_view" Value="xlsx" />
@@ -18,6 +18,9 @@
[CascadingParameter]
public Worksheet? Worksheet { get; set; }
[CascadingParameter]
public ISpreadsheet? Spreadsheet { get; set; }
private Workbook? Workbook => Worksheet?.Workbook;
[Parameter]

View File

@@ -2,7 +2,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetFormatToggleToolBase
<RadzenToggleButton Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="strikethrough_s" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenToggleButton AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Strikethrough))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Strikethrough))) Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="strikethrough_s" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
ToggleVariant="Variant.Flat" ToggleButtonStyle="ButtonStyle.Primary" ToggleShade="Shade.Lighter" />
@code {

View File

@@ -5,9 +5,9 @@
<RadzenSelectBar Value=@CurrentTextAlign ValueChanged=@OnTextAlignChangedAsync TValue="Radzen.TextAlign" Disabled=@IsDisabled Size="ButtonSize.Small">
<Items>
<RadzenSelectBarItem Value=@Radzen.TextAlign.Left Icon="format_align_left" />
<RadzenSelectBarItem Value=@Radzen.TextAlign.Center Icon="format_align_center" />
<RadzenSelectBarItem Value=@Radzen.TextAlign.Right Icon="format_align_right" />
<RadzenSelectBarItem Value=@Radzen.TextAlign.Left Icon="format_align_left" AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AlignLeft))) />
<RadzenSelectBarItem Value=@Radzen.TextAlign.Center Icon="format_align_center" AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AlignCenter))) />
<RadzenSelectBarItem Value=@Radzen.TextAlign.Right Icon="format_align_right" AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AlignRight))) />
</Items>
</RadzenSelectBar>

View File

@@ -2,7 +2,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetFormatToggleToolBase
<RadzenToggleButton Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="wrap_text" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenToggleButton AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_TextWrap))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_TextWrap))) Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="wrap_text" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
ToggleVariant="Variant.Flat" ToggleButtonStyle="ButtonStyle.Primary" ToggleShade="Shade.Lighter" />
@code {

View File

@@ -2,7 +2,7 @@
@using Radzen.Documents.Spreadsheet
@inherits RadzenSpreadsheetFormatToggleToolBase
<RadzenToggleButton Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="format_underlined" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
<RadzenToggleButton AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Underline))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Underline))) Value=@IsToggled ValueChanged=@OnToggledChangedAsync Icon="format_underlined" Disabled=@IsDisabled Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small"
ToggleVariant="Variant.Flat" ToggleButtonStyle="ButtonStyle.Primary" ToggleShade="Shade.Lighter" />
@code {

View File

@@ -3,7 +3,7 @@
@using Radzen.Documents.Spreadsheet
@implements IDisposable
<RadzenButton Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnUndoAsync Icon="undo" ButtonType="ButtonType.Button" Disabled=@IsDisabled />
<RadzenButton aria-label=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Undo))) title=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_Undo))) Variant="Variant.Text" ButtonStyle="ButtonStyle.Base" Size="ButtonSize.Small" Click=@OnUndoAsync Icon="undo" ButtonType="ButtonType.Button" Disabled=@IsDisabled />
@code {
#nullable enable

View File

@@ -5,9 +5,9 @@
<RadzenSelectBar Value=@CurrentVerticalAlign ValueChanged=@OnVerticalAlignChangedAsync TValue="Radzen.VerticalAlign" Disabled=@IsDisabled Size="ButtonSize.Small">
<Items>
<RadzenSelectBarItem Value=@Radzen.VerticalAlign.Top Icon="align_vertical_top" />
<RadzenSelectBarItem Value=@Radzen.VerticalAlign.Middle Icon="align_vertical_center" />
<RadzenSelectBarItem Value=@Radzen.VerticalAlign.Bottom Icon="align_vertical_bottom" />
<RadzenSelectBarItem Value=@Radzen.VerticalAlign.Top Icon="align_vertical_top" AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AlignTop))) />
<RadzenSelectBarItem Value=@Radzen.VerticalAlign.Middle Icon="align_vertical_center" AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AlignMiddle))) />
<RadzenSelectBarItem Value=@Radzen.VerticalAlign.Bottom Icon="align_vertical_bottom" AriaLabel=@(Spreadsheet?.Localize(nameof(RadzenStrings.Spreadsheet_AlignBottom))) />
</Items>
</RadzenSelectBar>

View File

@@ -5842,10 +5842,11 @@ Radzen.createUpload = function(el, url, auto, multiple, parameterName, method, s
}};
};
class Spreadsheet {
constructor(element, dotNetRef, shortcuts) {
constructor(element, dotNetRef, shortcuts, globalShortcuts) {
this.element = element;
this.dotNetRef = dotNetRef;
this.shortcuts = shortcuts;
this.globalShortcuts = globalShortcuts || [];
this.rtl = Radzen.isRTL(element);
this.element.addEventListener('keydown', this.onKeyDown);
this.element.addEventListener('pointerdown', this.onPointerDown);
@@ -5977,6 +5978,10 @@ class Spreadsheet {
e.preventDefault();
const column = +e.target.dataset.column;
this.dotNetRef.invokeMethodAsync('OnColumnContextMenuAsync', { column, pointer: this.toEventArgs(e) });
} else if (e.target === this.element) {
// Keyboard-triggered (Shift+F10 / Menu key) lands on the grid root, not a cell. Suppress the
// browser's native menu; the C# Shift+F10 shortcut opens our menu at the active cell.
e.preventDefault();
}
}
@@ -6036,7 +6041,15 @@ class Spreadsheet {
key += e.code.replace('Key', '').replace('Digit', '').replace('Numpad', '');
if (this.shortcuts.includes(key)) {
// Grid context = focus on THIS spreadsheet's root or inside one of ITS cell/formula editors.
// Scope the editor check to this.element so a sibling spreadsheet on the same page can never
// match. Outside the grid (a toolbar button, sheet tab, ...) only global shortcuts act, so Tab
// etc. stay native and the chrome is keyboard-navigable.
const editorInput = e.target.closest && e.target.closest('.rz-spreadsheet-editor-input');
const isGridContext = e.target === this.element ||
(editorInput != null && this.element.contains(editorInput));
if (this.globalShortcuts.includes(key) || (isGridContext && this.shortcuts.includes(key))) {
e.preventDefault();
}
@@ -6047,7 +6060,72 @@ class Spreadsheet {
e.preventDefault();
}
this.invokeAsync('OnKeyDownAsync', e);
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).
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
// matched.
const root = this.element;
const regions = [
root.querySelector('[role="tablist"], .rz-tabs-nav'),
root.querySelector('.rz-toolbar'),
root.querySelector('.rz-spreadsheet-formula-editor .rz-spreadsheet-editor-input'),
root,
root.querySelector('.rz-spreadsheet-sheet-tabs')
].filter(r => r);
if (regions.length === 0) {
return;
}
// Regions nest (the ribbon tabview contains the toolbar; the root contains everything), so pick
// the INNERMOST region that contains the focused element - otherwise an outer region captures
// focus that belongs to a nested one and F6 gets stuck cycling between two regions.
const active = document.activeElement;
let index = -1;
let best = null;
regions.forEach((r, i) => {
if ((r === active || r.contains(active)) && (best === null || best.contains(r))) {
best = r;
index = i;
}
});
if (index === -1) {
index = regions.indexOf(root);
}
const next = (index + (forward ? 1 : -1) + regions.length) % regions.length;
this.focusRegion(regions[next]);
}
focusRegion = (region) => {
if (region === this.element) {
region.focus();
return;
}
const focusable = region.matches('.rz-spreadsheet-editor-input')
? region
: region.querySelector('button:not([disabled]), a[href], input:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]');
(focusable || region).focus();
}
// Opens the cell context menu at the active cell (Shift+F10 / ContextMenu key). The cell element is
// scoped to this spreadsheet; a synthetic pointer carries its on-screen position.
openCellContextMenu = (row, column) => {
const cell = this.element.querySelector('.rz-spreadsheet-cell[data-row="' + row + '"][data-column="' + column + '"]');
const rect = (cell || this.element).getBoundingClientRect();
const pointer = {
clientX: Math.round(rect.left + 8),
clientY: Math.round(cell ? rect.bottom : rect.top + 8),
button: 0, buttons: 0, pointerType: 'mouse', isPrimary: true
};
this.dotNetRef.invokeMethodAsync('OnCellContextMenuAsync', { row, column, pointer });
}
invokeAsync(name, e) {
@@ -6057,7 +6135,9 @@ class Spreadsheet {
toEventArgs(e) {
return {
key: e.key,
code: e.code,
code: this.rtl && (e.code === 'ArrowLeft' || e.code === 'ArrowRight')
? (e.code === 'ArrowLeft' ? 'ArrowRight' : 'ArrowLeft')
: e.code,
location: e.location,
repeat: e.repeat,
ctrlKey: e.ctrlKey,
@@ -6248,7 +6328,7 @@ class SheetEditor {
e.stopPropagation();
e.preventDefault();
this.dotNetRef.invokeMethodAsync('OnKeyDownAsync', { key: e.key });
} else if (e.key != 'Enter' && e.key != 'Escape' && e.key != 'Tab') {
} else if (e.key != 'Enter' && e.key != 'Escape' && e.key != 'Tab' && e.key != 'F6') {
e.stopPropagation();
}
}
@@ -6291,7 +6371,7 @@ class SheetEditor {
Radzen.createSheetEditor = (element, value, autoFocus, dotNetRef) => new SheetEditor(element, value, autoFocus, dotNetRef);
Radzen.createSpreadsheet = (element, dotNetRef, shortcuts) => new Spreadsheet(element, dotNetRef, shortcuts);
Radzen.createSpreadsheet = (element, dotNetRef, shortcuts, globalShortcuts) => new Spreadsheet(element, dotNetRef, shortcuts, globalShortcuts);
Radzen.createVirtualItemContainer = (scrollable, content, ref) => {
var height = scrollable.clientHeight;
var width = scrollable.clientWidth;