From 5cde1a53438e46c96f328e38946fb8506e4645a9 Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:43:31 +1000 Subject: [PATCH] Fix DatePickerPresenter focusing wrong selector for day-first locales (#21116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: focus correct date selector for day-first locales in DatePickerPresenter SetInitialFocus had a copy-paste bug where the day-first branch focused _monthSelector instead of _daySelector. This caused every locale that puts day first (UK, Europe, Australia, India — dd/MM/yyyy) to focus the wrong selector when the picker opened. Refactored the three near-identical if/else branches into a data-driven loop that pairs each host panel with its selector, making this class of copy-paste bug structurally impossible. * chore: retrigger CI * chore: retrigger CI --------- Co-authored-by: Julien Lebosquain --- .../DateTimePickers/DatePickerPresenter.cs | 34 ++++++++------ .../DatePickerTests.cs | 44 +++++++++++++++++++ 2 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index eac695b0b1..cbfb6467a0 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -473,22 +473,30 @@ namespace Avalonia.Controls private void SetInitialFocus(TemplateItems items) { - int monthCol = MonthVisible ? Grid.GetColumn(items._monthHost) : int.MaxValue; - int dayCol = DayVisible ? Grid.GetColumn(items._dayHost) : int.MaxValue; - int yearCol = YearVisible ? Grid.GetColumn(items._yearHost) : int.MaxValue; + ReadOnlySpan<(bool visible, Panel host, DateTimePickerPanel selector)> candidates = + [ + (MonthVisible, items._monthHost, items._monthSelector), + (DayVisible, items._dayHost, items._daySelector), + (YearVisible, items._yearHost, items._yearSelector), + ]; - if (monthCol < dayCol && monthCol < yearCol) - { - items._monthSelector.Focus(NavigationMethod.Pointer); - } - else if (dayCol < monthCol && dayCol < yearCol) - { - items._monthSelector.Focus(NavigationMethod.Pointer); - } - else if (yearCol < monthCol && yearCol < dayCol) + DateTimePickerPanel? leftmost = null; + var minCol = int.MaxValue; + + foreach (var (visible, host, selector) in candidates) { - items._yearSelector.Focus(NavigationMethod.Pointer); + if (!visible) + continue; + + var col = Grid.GetColumn(host); + if (col < minCol) + { + minCol = col; + leftmost = selector; + } } + + leftmost?.Focus(NavigationMethod.Pointer); } private void OnDismissButtonClicked(object? sender, RoutedEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index fc0c64d5d9..4688a88fc5 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Linq; using System.Reactive.Subjects; using Avalonia.Controls.Primitives; @@ -7,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Harfbuzz; using Avalonia.Headless; +using Avalonia.Input; using Avalonia.Platform; using Avalonia.Threading; using Avalonia.UnitTests; @@ -272,12 +274,54 @@ namespace Avalonia.Controls.UnitTests Assert.NotEqual(previousOffset, panel.Offset); } + [Fact] + public void SetInitialFocus_Should_Focus_Day_Selector_For_Day_First_Locale() + { + var previousCulture = CultureInfo.CurrentCulture; + try + { + // en-GB uses dd/MM/yyyy — day appears first in the short date pattern + CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB"); + + using (UnitTestApplication.Start(FocusServices)) + { + var presenter = new DatePickerPresenter { Template = CreatePickerTemplate() }; + var root = new TestRoot(presenter); + root.LayoutManager.ExecuteInitialLayoutPass(); + + // Trigger InitPicker again now that the visual tree is fully connected, + // so SetInitialFocus can successfully call Focus(). + presenter.Date = new DateTimeOffset(2024, 6, 15, 0, 0, 0, TimeSpan.Zero); + + var daySelector = presenter + .GetVisualDescendants() + .OfType() + .First(p => p.Name == "PART_DaySelector"); + + Assert.Same(daySelector, root.FocusManager.GetFocusedElement()); + } + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } + private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new HeadlessFontManagerStub(), standardCursorFactory: Mock.Of(), textShaperImpl: new HarfBuzzTextShaper(), renderInterface: new HeadlessPlatformRenderInterface()); + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( + fontManagerImpl: new HeadlessFontManagerStub(), + standardCursorFactory: Mock.Of(), + textShaperImpl: new HarfBuzzTextShaper(), + renderInterface: new HeadlessPlatformRenderInterface(), + keyboardDevice: () => new KeyboardDevice(), + keyboardNavigation: () => new KeyboardNavigationHandler(), + inputManager: new InputManager()); + private static IControlTemplate CreateTemplate() { return new FuncControlTemplate((control, scope) =>