From 08f92993900fc4032fcb72813ad627bbb27d5c6b Mon Sep 17 00:00:00 2001 From: Vladimir Enchev Date: Wed, 8 Jul 2026 14:03:16 +0300 Subject: [PATCH] keyboard navigation improved --- Radzen.Blazor.Tests/DatePickerTests.cs | 58 +++++++++ Radzen.Blazor/RadzenDatePicker.razor | 6 +- Radzen.Blazor/RadzenDatePicker.razor.cs | 118 ++++++++++++++++++ RadzenBlazorDemos/Pages/DatePickerPage.razor | 3 + .../Pages/DateRangePickerPage.razor | 5 + 5 files changed, 188 insertions(+), 2 deletions(-) diff --git a/Radzen.Blazor.Tests/DatePickerTests.cs b/Radzen.Blazor.Tests/DatePickerTests.cs index a0e2cbc29..3018e99c6 100644 --- a/Radzen.Blazor.Tests/DatePickerTests.cs +++ b/Radzen.Blazor.Tests/DatePickerTests.cs @@ -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>(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>(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() { diff --git a/Radzen.Blazor/RadzenDatePicker.razor b/Radzen.Blazor/RadzenDatePicker.razor index 1800e8bba..6f97b107a 100644 --- a/Radzen.Blazor/RadzenDatePicker.razor +++ b/Radzen.Blazor/RadzenDatePicker.razor @@ -80,7 +80,8 @@ { var isActiveMonth = month.Value == GetCalendarMonth(CurrentDate); var monthDisabled = Disabled || IsMonthDisabled(month.Value); - + } @@ -92,7 +93,8 @@ @foreach (var year in YearsInView) { var isActiveYear = year.Value == GetCalendarYear(CurrentDate); - + } diff --git a/Radzen.Blazor/RadzenDatePicker.razor.cs b/Radzen.Blazor/RadzenDatePicker.razor.cs index aba635740..8c120b2b0 100644 --- a/Radzen.Blazor/RadzenDatePicker.razor.cs +++ b/Radzen.Blazor/RadzenDatePicker.razor.cs @@ -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) diff --git a/RadzenBlazorDemos/Pages/DatePickerPage.razor b/RadzenBlazorDemos/Pages/DatePickerPage.razor index 0143af65f..9fa557d44 100644 --- a/RadzenBlazorDemos/Pages/DatePickerPage.razor +++ b/RadzenBlazorDemos/Pages/DatePickerPage.razor @@ -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." } }; } diff --git a/RadzenBlazorDemos/Pages/DateRangePickerPage.razor b/RadzenBlazorDemos/Pages/DateRangePickerPage.razor index ff41d69b2..7a28cb6a8 100644 --- a/RadzenBlazorDemos/Pages/DateRangePickerPage.razor +++ b/RadzenBlazorDemos/Pages/DateRangePickerPage.razor @@ -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." } }; }