From 7c2f1869bc514a1053beac612d1dac109d8caf00 Mon Sep 17 00:00:00 2001 From: Evan <109839359+Evan260@users.noreply.github.com> Date: Fri, 27 Feb 2026 08:52:30 -0600 Subject: [PATCH] Fix CalendarDatePicker becoming unusable when closed programmatically (#20756) * Fix CalendarDatePicker becoming unusable when closed programmatically * Add unit test for CalendarDatePicker --- .../Calendar/CalendarItem.cs | 13 ++++ .../CalendarTests.cs | 75 ++++++++++++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index 2bfe3c9be2..3d548e15b9 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -299,6 +299,19 @@ namespace Avalonia.Controls.Primitives } } + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + // Reset mouse button tracking state. When the calendar popup closes + // (e.g. due to a programmatic window change during date selection), + // the PointerReleased event never fires, leaving these flags stuck. + // See https://github.com/AvaloniaUI/Avalonia/issues/18418 + _isMouseLeftButtonDown = false; + _isMouseLeftButtonDownYearView = false; + } + private void SetDayTitles() { for (int childIndex = 0; childIndex < Calendar.ColumnsPerMonth; childIndex++) diff --git a/tests/Avalonia.Controls.UnitTests/CalendarTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarTests.cs index 5050285b0b..75ba43c9e4 100644 --- a/tests/Avalonia.Controls.UnitTests/CalendarTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CalendarTests.cs @@ -1,8 +1,9 @@ using Xunit; using System; -using System.Collections; using System.Collections.Generic; using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Input; using Avalonia.UnitTests; namespace Avalonia.Controls.UnitTests @@ -367,5 +368,77 @@ namespace Avalonia.Controls.UnitTests Assert.True(calendar.SelectedDates.Contains(earlierDate)); Assert.True(calendar.SelectedDates.Contains(laterDate)); } + + [Fact] + public void CalendarItem_Should_Reset_Mouse_Down_Flag_On_Detach_From_Visual_Tree() + { + var calendar = new Calendar(); + calendar.SelectionMode = CalendarSelectionMode.SingleDate; + + var calendarItem = new CalendarItem(); + calendarItem.Owner = calendar; + + // Attach CalendarItem to a visual tree + var root = new TestRoot(calendarItem); + + // Create a day button and simulate mouse left button down, + // which sets the internal _isMouseLeftButtonDown flag to true. + var date1 = new DateTime(2024, 1, 15); + var dayButton1 = new CalendarDayButton { DataContext = date1 }; + + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + var props = new PointerPointProperties(RawInputModifiers.LeftMouseButton, + PointerUpdateKind.LeftButtonPressed); + var pressArgs = new PointerPressedEventArgs(dayButton1, pointer, root, + default, 0, props, KeyModifiers.None); + + calendarItem.Cell_MouseLeftButtonDown(dayButton1, pressArgs); + + // date1 should now be selected + Assert.Equal(1, calendar.SelectedDates.Count); + Assert.Equal(date1, calendar.SelectedDates[0]); + + // Detach CalendarItem from visual tree (simulates popup closing + // during date selection without a PointerReleased event). + root.Child = null; + + // Create a different day button and simulate mouse enter. + // Before the fix, _isMouseLeftButtonDown would still be true, + // causing hover to auto-select dates. + var date2 = new DateTime(2024, 1, 20); + var dayButton2 = new CalendarDayButton { DataContext = date2 }; + + calendarItem.Cell_MouseEntered(dayButton2, null!); + + // The selected date should NOT have changed to date2, + // because the mouse-down flag was reset when detaching. + Assert.Equal(1, calendar.SelectedDates.Count); + Assert.Equal(date1, calendar.SelectedDates[0]); + } + + [Fact] + public void CalendarItem_Should_Reset_YearView_Mouse_Down_Flag_On_Detach_From_Visual_Tree() + { + var calendar = new Calendar(); + var calendarItem = new CalendarItem(); + calendarItem.Owner = calendar; + + // Attach CalendarItem to a visual tree + var root = new TestRoot(calendarItem); + + // Use reflection to set the _isMouseLeftButtonDownYearView flag, + // since Month_CalendarButtonMouseDown is private. + var field = typeof(CalendarItem).GetField("_isMouseLeftButtonDownYearView", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull(field); + field!.SetValue(calendarItem, true); + Assert.True((bool)field.GetValue(calendarItem)!); + + // Detach CalendarItem from visual tree + root.Child = null; + + // Verify the flag was reset + Assert.False((bool)field.GetValue(calendarItem)!); + } } }