mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
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:
committed by
Atanas Korchev
parent
e17f1113e7
commit
8befb201ef
315
Radzen.Blazor.Tests/Spreadsheet/SpreadsheetAccessibilityTests.cs
Normal file
315
Radzen.Blazor.Tests/Spreadsheet/SpreadsheetAccessibilityTests.cs
Normal 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
|
||||
}
|
||||
}
|
||||
23
Radzen.Blazor/RadzenLiveRegion.razor
Normal file
23
Radzen.Blazor/RadzenLiveRegion.razor
Normal 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; }
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
67
Radzen.Blazor/RadzenStrings.Designer.cs
generated
67
Radzen.Blazor/RadzenStrings.Designer.cs
generated
@@ -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); } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
54
Radzen.Blazor/Spreadsheet/DrawingSizeDialog.razor
Normal file
54
Radzen.Blazor/Spreadsheet/DrawingSizeDialog.razor
Normal 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));
|
||||
}
|
||||
@@ -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>
|
||||
47
Radzen.Blazor/Spreadsheet/ResizeAxisCommands.cs
Normal file
47
Radzen.Blazor/Spreadsheet/ResizeAxisCommands.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
8
Radzen.Blazor/Spreadsheet/SpreadsheetAccessibility.razor
Normal file
8
Radzen.Blazor/Spreadsheet/SpreadsheetAccessibility.razor
Normal 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" />
|
||||
219
Radzen.Blazor/Spreadsheet/SpreadsheetAccessibility.razor.cs
Normal file
219
Radzen.Blazor/Spreadsheet/SpreadsheetAccessibility.razor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
39
Radzen.Blazor/Spreadsheet/SpreadsheetShortcutsDialog.razor
Normal file
39
Radzen.Blazor/Spreadsheet/SpreadsheetShortcutsDialog.razor
Normal 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; }
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user