keyboard navigation improved

This commit is contained in:
Vladimir Enchev
2026-07-08 14:03:16 +03:00
parent ac9fe3d394
commit 08f9299390
5 changed files with 188 additions and 2 deletions

View File

@@ -938,6 +938,64 @@ namespace Radzen.Blazor.Tests
Assert.Contains("2025", component.Find(".rz-calendar-title-button").TextContent);
}
[Fact]
public void DatePicker_DrillDown_MonthGrid_Supports_Keyboard_Navigation()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameters =>
{
parameters.Add(p => p.NavigationMode, DatePickerNavigationMode.DrillDown);
parameters.Add(p => p.InitialViewDate, new DateTime(2024, 5, 1));
});
component.InvokeAsync(() => component.Find(".rz-calendar-title-button").Click());
var cells = component.FindAll(".rz-calendar-month-cell");
Assert.Equal("0", cells[4].GetAttribute("tabindex"));
component.InvokeAsync(() => component.FindAll(".rz-calendar-month-cell")[4].KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Code = "ArrowRight" }));
cells = component.FindAll(".rz-calendar-month-cell");
Assert.Equal("0", cells[5].GetAttribute("tabindex"));
Assert.Equal("-1", cells[4].GetAttribute("tabindex"));
component.InvokeAsync(() => component.FindAll(".rz-calendar-month-cell")[5].KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Code = "ArrowDown" }));
Assert.Equal("0", component.FindAll(".rz-calendar-month-cell")[8].GetAttribute("tabindex"));
component.InvokeAsync(() => component.FindAll(".rz-calendar-month-cell")[8].KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Code = "PageDown" }));
Assert.Contains("2025", component.Find(".rz-calendar-title-button").TextContent);
Assert.Equal("0", component.FindAll(".rz-calendar-month-cell")[8].GetAttribute("tabindex"));
}
[Fact]
public void DatePicker_DrillDown_YearGrid_Supports_Keyboard_Navigation()
{
using var ctx = new TestContext();
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
ctx.JSInterop.SetupModule("_content/Radzen.Blazor/Radzen.Blazor.js");
var component = ctx.RenderComponent<RadzenDatePicker<DateTime>>(parameters =>
{
parameters.Add(p => p.NavigationMode, DatePickerNavigationMode.DrillDown);
parameters.Add(p => p.InitialViewDate, new DateTime(2024, 5, 1));
});
component.InvokeAsync(() => component.Find(".rz-calendar-title-button").Click());
component.InvokeAsync(() => component.Find(".rz-calendar-title-button").Click());
var focused = component.FindAll(".rz-calendar-year-cell").Single(c => c.GetAttribute("tabindex") == "0");
Assert.Equal("2024", focused.TextContent);
component.InvokeAsync(() => component.FindAll(".rz-calendar-year-cell").Single(c => c.GetAttribute("tabindex") == "0").KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Code = "ArrowRight" }));
Assert.Equal("2025", component.FindAll(".rz-calendar-year-cell").Single(c => c.GetAttribute("tabindex") == "0").TextContent);
component.InvokeAsync(() => component.FindAll(".rz-calendar-year-cell").Single(c => c.GetAttribute("tabindex") == "0").KeyDown(new Microsoft.AspNetCore.Components.Web.KeyboardEventArgs { Code = "PageDown" }));
Assert.Equal("2037", component.FindAll(".rz-calendar-year-cell").Single(c => c.GetAttribute("tabindex") == "0").TextContent);
Assert.Contains(component.FindAll(".rz-calendar-year-cell"), c => c.TextContent == "2034");
}
[Fact]
public void DatePicker_DrillDown_Disables_Months_Outside_MinMax()
{

View File

@@ -80,7 +80,8 @@
{
var isActiveMonth = month.Value == GetCalendarMonth(CurrentDate);
var monthDisabled = Disabled || IsMonthDisabled(month.Value);
<button type="button" class="@($"rz-calendar-month-cell{(isActiveMonth ? " rz-state-active" : "")}")" disabled="@monthDisabled" aria-disabled="@(monthDisabled ? "true" : "false")" aria-pressed="@(isActiveMonth ? "true" : "false")" @onclick="@(() => SelectViewMonth(month.Value))">@month.Name</button>
<button type="button" id="@($"{GetId()}-month-{month.Value}")" class="@($"rz-calendar-month-cell{(isActiveMonth ? " rz-state-active" : "")}")" disabled="@monthDisabled" aria-disabled="@(monthDisabled ? "true" : "false")" aria-pressed="@(isActiveMonth ? "true" : "false")" tabindex="@(isActiveMonth && !monthDisabled ? TabIndex : -1)"
@onclick="@(() => SelectViewMonth(month.Value))" @onkeydown="@OnMonthCellKeyDown" @onkeydown:preventDefault="preventKeyPress" @onkeydown:stopPropagation="stopKeydownPropagation">@month.Name</button>
}
</div>
</div>
@@ -92,7 +93,8 @@
@foreach (var year in YearsInView)
{
var isActiveYear = year.Value == GetCalendarYear(CurrentDate);
<button type="button" class="@($"rz-calendar-year-cell{(isActiveYear ? " rz-state-active" : "")}")" disabled="@Disabled" aria-disabled="@(Disabled ? "true" : "false")" aria-pressed="@(isActiveYear ? "true" : "false")" @onclick="@(() => SelectViewYear(year.Value))">@year.Name</button>
<button type="button" id="@($"{GetId()}-year-{year.Value}")" class="@($"rz-calendar-year-cell{(isActiveYear ? " rz-state-active" : "")}")" disabled="@Disabled" aria-disabled="@(Disabled ? "true" : "false")" aria-pressed="@(isActiveYear ? "true" : "false")" tabindex="@(isActiveYear ? TabIndex : -1)"
@onclick="@(() => SelectViewYear(year.Value))" @onkeydown="@OnYearCellKeyDown" @onkeydown:preventDefault="preventKeyPress" @onkeydown:stopPropagation="stopKeydownPropagation">@year.Name</button>
}
</div>
</div>

View File

@@ -258,6 +258,104 @@ namespace Radzen.Blazor
currentView = CalendarViewKind.Days;
}
bool shouldFocusMonthCell;
bool shouldFocusYearCell;
void OnMonthCellKeyDown(KeyboardEventArgs args)
{
var key = args.Code != null ? args.Code : args.Key;
var delta = key switch
{
"ArrowLeft" => -1,
"ArrowRight" => 1,
"ArrowUp" => -3,
"ArrowDown" => 3,
_ => 0
};
if (delta != 0)
{
preventKeyPress = true;
stopKeydownPropagation = true;
NavigateMonth(delta);
shouldFocusMonthCell = true;
return;
}
if (key == "PageUp" || key == "PageDown")
{
preventKeyPress = true;
stopKeydownPropagation = true;
NavigateYear(key == "PageUp" ? -1 : 1);
shouldFocusMonthCell = true;
return;
}
if (key == "Home" || key == "End")
{
preventKeyPress = true;
stopKeydownPropagation = true;
SetMonth(key == "Home" ? 1 : Culture.Calendar.GetMonthsInYear(GetCalendarYear(CurrentDate)));
shouldFocusMonthCell = true;
return;
}
preventKeyPress = false;
stopKeydownPropagation = false;
}
void OnYearCellKeyDown(KeyboardEventArgs args)
{
var key = args.Code != null ? args.Code : args.Key;
var delta = key switch
{
"ArrowLeft" => -1,
"ArrowRight" => 1,
"ArrowUp" => -3,
"ArrowDown" => 3,
_ => 0
};
if (delta != 0)
{
preventKeyPress = true;
stopKeydownPropagation = true;
NavigateYear(delta);
shouldFocusYearCell = true;
return;
}
if (key == "PageUp" || key == "PageDown")
{
preventKeyPress = true;
stopKeydownPropagation = true;
NavigateYearsPage(key == "PageUp" ? -1 : 1);
shouldFocusYearCell = true;
return;
}
if (key == "Home" || key == "End")
{
preventKeyPress = true;
stopKeydownPropagation = true;
var target = key == "Home" ? YearsPageStart : Math.Min(YearsPageStart + YearsPerPage - 1, YearTo);
if (target != GetCalendarYear(CurrentDate))
{
SetYear(target);
}
shouldFocusYearCell = true;
return;
}
preventKeyPress = false;
stopKeydownPropagation = false;
}
internal void NavigateMonth(int direction)
{
if (Disabled)
@@ -2511,6 +2609,26 @@ namespace Radzen.Blazor
}
catch { }
}
if (shouldFocusMonthCell && JSRuntime != null)
{
shouldFocusMonthCell = false;
try
{
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", $"{GetId()}-month-{GetCalendarMonth(CurrentDate)}");
}
catch { }
}
if (shouldFocusYearCell && JSRuntime != null)
{
shouldFocusYearCell = false;
try
{
await JSRuntime.InvokeVoidAsync("Radzen.focusElement", $"{GetId()}-year-{GetCalendarYear(CurrentDate)}");
}
catch { }
}
}
private void ValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)

View File

@@ -225,6 +225,9 @@
new KeyboardShortcut { Key = "Shift + PageUp on open popup", Action = "Move focus to the same day of the previous year." },
new KeyboardShortcut { Key = "Shift + PageDown on open popup", Action = "Move focus to the same day of the next year." },
new KeyboardShortcut { Key = "Enter or Space in an opened popup", Action = "Select the focused day and close the popup." },
new KeyboardShortcut { Key = "Enter or Space on the calendar title (drill-down navigation)", Action = "Drill up to month selection, then to year selection." },
new KeyboardShortcut { Key = "Arrow keys in month or year selection (drill-down navigation)", Action = "Move focus between months or years." },
new KeyboardShortcut { Key = "PageUp or PageDown in month or year selection (drill-down navigation)", Action = "Move focus to the previous or next year or page of years." },
new KeyboardShortcut { Key = "Esc in an opened popup", Action = "Close the popup." }
};
}

View File

@@ -75,6 +75,11 @@
new KeyboardShortcut { Key = "Shift + PageUp on open popup", Action = "Move focus to the same day of the previous year." },
new KeyboardShortcut { Key = "Shift + PageDown on open popup", Action = "Move focus to the same day of the next year." },
new KeyboardShortcut { Key = "Enter or Space in an opened popup", Action = "Select the focused day as start or end of the range. The popup closes when the range is complete." },
new KeyboardShortcut { Key = "Enter or Space on the calendar title", Action = "Drill up to month selection, then to year selection." },
new KeyboardShortcut { Key = "Arrow keys in month or year selection", Action = "Move focus between months or years." },
new KeyboardShortcut { Key = "PageUp or PageDown in month selection", Action = "Move focus to the same month of the previous or next year." },
new KeyboardShortcut { Key = "PageUp or PageDown in year selection", Action = "Move focus to the previous or next page of years." },
new KeyboardShortcut { Key = "Enter or Space on a month or year", Action = "Select it and drill back down." },
new KeyboardShortcut { Key = "Esc in an opened popup", Action = "Close the popup." }
};
}