From ddb4b21c6315faddfdbb22c419de0083c106107c Mon Sep 17 00:00:00 2001 From: Corvin <43533385+corvinsz@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:38:08 +0100 Subject: [PATCH 1/2] fix(DialogHost): restore focus inside dialog after window is minimized and restored --- src/MaterialDesignThemes.Wpf/DialogHost.cs | 48 ++++++++++ .../DialogHost/WithMultipleTextBoxes.xaml | 27 ++++++ .../DialogHost/WithMultipleTextBoxes.xaml.cs | 16 ++++ .../WPF/DialogHosts/DialogHostTests.cs | 89 +++++++++++++++++++ 4 files changed, 180 insertions(+) create mode 100644 tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml create mode 100644 tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml.cs diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs index f0b39e45b9..ed300694ea 100644 --- a/src/MaterialDesignThemes.Wpf/DialogHost.cs +++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs @@ -75,6 +75,8 @@ public class DialogHost : ContentControl private DialogClosingEventHandler? _attachedDialogClosingEventHandler; private DialogClosedEventHandler? _attachedDialogClosedEventHandler; private IInputElement? _restoreFocusDialogClose; + private Window? _window; + private UIElement? _lastFocusedPopupElement; private Action? _currentSnackbarMessageQueueUnPauseAction; static DialogHost() @@ -1010,8 +1012,50 @@ private void CloseDialogHandler(object sender, ExecutedRoutedEventArgs executedR private string GetStateName() => IsOpen ? OpenStateName : ClosedStateName; + protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + if (e.NewFocus is UIElement element && + _popup?.Child is { } child && + child.IsAncestorOf(element)) + { + _lastFocusedPopupElement = element; + } + base.OnGotKeyboardFocus(e); + } + + protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + // Minimizing moves focus to null; only forget the element when the user focuses something else in this window + if (e.NewFocus is DependencyObject newFocus && + _popup?.Child is { } child && + !child.IsAncestorOf(newFocus) && + Window.GetWindow(newFocus) == _window) + { + _lastFocusedPopupElement = null; + } + base.OnLostKeyboardFocus(e); + } + + private void WindowOnActivated(object? sender, EventArgs e) + { + // The popup has its own window handle, so WPF does not restore focus into it when the window is re-activated (issue #3434) + Dispatcher.BeginInvoke(() => + { + if (IsOpen && + _popup is { IsOpen: true, Child: { IsKeyboardFocusWithin: false } child } && + _lastFocusedPopupElement is { IsVisible: true } element && + child.IsAncestorOf(element)) + { + element.Focus(); + } + }, DispatcherPriority.Input); + } + private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) { + _window?.Activated -= WindowOnActivated; + _window = null; + foreach (var weakRef in LoadedInstances.ToList()) { if (!weakRef.TryGetTarget(out DialogHost? dialogHost) || ReferenceEquals(dialogHost, this)) @@ -1024,6 +1068,10 @@ private void OnUnloaded(object sender, RoutedEventArgs routedEventArgs) private void OnLoaded(object sender, RoutedEventArgs routedEventArgs) { + _window?.Activated -= WindowOnActivated; + _window = Window.GetWindow(this); + _window?.Activated += WindowOnActivated; + foreach (var weakRef in LoadedInstances.ToList()) { if (weakRef.TryGetTarget(out DialogHost? dialogHost) && ReferenceEquals(dialogHost, this)) diff --git a/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml b/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml new file mode 100644 index 0000000000..6206c001ea --- /dev/null +++ b/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml @@ -0,0 +1,27 @@ + + + + + + + + + + + diff --git a/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml.cs b/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml.cs new file mode 100644 index 0000000000..0911fa7f73 --- /dev/null +++ b/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml.cs @@ -0,0 +1,16 @@ +namespace MaterialDesignThemes.UITests.Samples.DialogHost; + +/// +/// Interaction logic for WithMultipleTextBoxes.xaml +/// +public partial class WithMultipleTextBoxes : UserControl +{ + public WithMultipleTextBoxes() + { + InitializeComponent(); + } + private void DialogHost_Loaded(object sender, RoutedEventArgs e) + { + SampleDialogHost.IsOpen = true; + } +} diff --git a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs index fa84783942..78336475f8 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs @@ -665,6 +665,95 @@ public async Task DialogHost_FocusMovedInsideDialogWhileOpening_IsKept(string di } } + [Test] + [Description("Issue 3434")] + [Arguments(WindowState.Minimized, WindowState.Maximized, null)] + [Arguments(WindowState.Minimized, WindowState.Normal, null)] + [Arguments(WindowState.Minimized, WindowState.Maximized, "MaterialDesignEmbeddedDialogHost")] + [Arguments(WindowState.Minimized, WindowState.Normal, "MaterialDesignEmbeddedDialogHost")] + public async Task DialogHost_WhenWindowStateChanges_FocusedElementStaysFocused(WindowState firstWindowState, + WindowState secondWindowState, + string? styleName) + { + await using var recorder = new TestRecorder(App); + + var dialogHost = (await LoadUserControl()).As(); + if (styleName is not null) + { + await dialogHost.RemoteExecute(SetDialogHostStyle, styleName); + } + await Task.Delay(400); + + // Select the second TextBox + var tbTwo = await dialogHost.GetElement("TextBoxTwo"); + await tbTwo.MoveKeyboardFocus(); + await Assert.That(await tbTwo.GetIsFocused()).IsTrue(); + + // First state + await dialogHost.RemoteExecute(SetStateOfParentWindow, firstWindowState); + await Task.Delay(400); + // Second state + await dialogHost.RemoteExecute(SetStateOfParentWindow, secondWindowState); + await Task.Delay(400); + + // After changing state of the window the previously focused element should be focused again + await Assert.That(await tbTwo.GetIsFocused()).IsTrue(); + recorder.Success(); + } + + [Test] + [Description("Issue 3434")] + //[Arguments(null)] + [Arguments("MaterialDesignEmbeddedDialogHost")] + public async Task DialogHost_WhenWindowStateChanges_FocusOutsideDialogStaysOutside(string? styleName) + { + await using var recorder = new TestRecorder(App); + + var grid = await LoadXaml(""" + + + + + + + + + + + + + """); + var dialogHost = await grid.GetElement("DialogHost"); + if (styleName is not null) + { + await dialogHost.RemoteExecute(SetDialogHostStyle, styleName); + } + await Task.Delay(400); + + var insideTextBox = await dialogHost.GetElement("InsideTextBox"); + var outsideTextBox = await grid.GetElement("OutsideTextBox"); + await insideTextBox.MoveKeyboardFocus(); + await Assert.That(await insideTextBox.GetIsFocused()).IsTrue(); + await outsideTextBox.MoveKeyboardFocus(); + await Assert.That(await outsideTextBox.GetIsFocused()).IsTrue(); + + await dialogHost.RemoteExecute(SetStateOfParentWindow, WindowState.Minimized); + await Task.Delay(400); + await dialogHost.RemoteExecute(SetStateOfParentWindow, WindowState.Normal); + await Task.Delay(400); + + // Focus was outside the dialog before minimizing, so it must not be moved into the dialog + await Assert.That(await outsideTextBox.GetIsFocused()).IsTrue(); + recorder.Success(); + } + + private static object SetStateOfParentWindow(DialogHost dialogHost, WindowState state) + { + var window = Window.GetWindow(dialogHost); + window.WindowState = state; + return null!; + } + private static object SetDialogHostStyle(DialogHost dialogHost, string styleName) { var style = (Style)dialogHost.FindResource(styleName); From 6f339ebc89e4cfa839f9ace3546acfc458cc0a0b Mon Sep 17 00:00:00 2001 From: Corvin <43533385+corvinsz@users.noreply.github.com> Date: Sun, 8 Nov 2026 13:49:04 +0100 Subject: [PATCH 2/2] comment in test case --- .../WPF/DialogHosts/DialogHostTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs index 78336475f8..e376fd7d83 100644 --- a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs +++ b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs @@ -703,7 +703,7 @@ public async Task DialogHost_WhenWindowStateChanges_FocusedElementStaysFocused(W [Test] [Description("Issue 3434")] - //[Arguments(null)] + [Arguments(null)] [Arguments("MaterialDesignEmbeddedDialogHost")] public async Task DialogHost_WhenWindowStateChanges_FocusOutsideDialogStaysOutside(string? styleName) {