Browse Source

Fix DatePickerPresenter focusing wrong selector for day-first locales (#21116)

* 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 <julien@lebosquain.net>
pull/21137/head
Nathan Nguyen 6 months ago
committed by GitHub
parent
commit
5cde1a5343
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 34
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  2. 44
      tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

34
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)

44
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<DateTimePickerPanel>()
.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<ICursorFactory>(),
textShaperImpl: new HarfBuzzTextShaper(),
renderInterface: new HeadlessPlatformRenderInterface());
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
fontManagerImpl: new HeadlessFontManagerStub(),
standardCursorFactory: Mock.Of<ICursorFactory>(),
textShaperImpl: new HarfBuzzTextShaper(),
renderInterface: new HeadlessPlatformRenderInterface(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: () => new KeyboardNavigationHandler(),
inputManager: new InputManager());
private static IControlTemplate CreateTemplate()
{
return new FuncControlTemplate((control, scope) =>

Loading…
Cancel
Save