diff --git a/src/MaterialDesignThemes.Wpf/DialogHost.cs b/src/MaterialDesignThemes.Wpf/DialogHost.cs
index 1fb5688e3a..4cbb9db755 100644
--- a/src/MaterialDesignThemes.Wpf/DialogHost.cs
+++ b/src/MaterialDesignThemes.Wpf/DialogHost.cs
@@ -64,6 +64,8 @@ public class DialogHost : ContentControl
private DialogClosingEventHandler? _attachedDialogClosingEventHandler;
private DialogClosedEventHandler? _attachedDialogClosedEventHandler;
private IInputElement? _restoreFocusDialogClose;
+ private IInputElement? _lastFocusedDialogElement;
+ private HwndSourceHook? _hook;
private Action? _currentSnackbarMessageQueueUnPauseAction;
static DialogHost()
@@ -370,6 +372,7 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj
dialogHost.CurrentSession = new DialogSession(dialogHost);
var window = Window.GetWindow(dialogHost);
+ dialogHost.ListenForWindowStateChanged(window);
if (!dialogHost.IsRestoreFocusDisabled)
{
dialogHost._restoreFocusDialogClose = window != null ? FocusManager.GetFocusedElement(window) : null;
@@ -395,7 +398,8 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj
//https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/issues/187
//totally not happy about this, but on immediate validation we can get some weird looking stuff...give WPF a kick to refresh...
- Task.Delay(300).ContinueWith(t => dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => {
+ Task.Delay(300).ContinueWith(t => dialogHost.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
+ {
CommandManager.InvalidateRequerySuggested();
//Delay focusing the popup until after the animation has some time, Issue #2912
UIElement? child = dialogHost.FocusPopup();
@@ -405,6 +409,56 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj
})));
}
+
+ private void ListenForWindowStateChanged(Window? window)
+ {
+ if (window is null)
+ {
+ return;
+ }
+
+ if (PresentationSource.FromVisual(window) is HwndSource source)
+ {
+ _hook = Hook;
+ source.RemoveHook(_hook);
+ source.AddHook(_hook);
+ }
+
+ IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
+ {
+ //https://learn.microsoft.com/en-us/windows/win32/menurc/wm-syscommand
+ const int WM_SYSCOMMAND = 0x0112;
+ const int SC_MINIMIZE = 0xf020;
+ const int SC_RESTORE = 0xF120;
+
+ long wParamLong = wParam.ToInt64();
+ switch (msg)
+ {
+ case WM_SYSCOMMAND:
+ if (wParamLong == SC_MINIMIZE && //Minimize
+ _popupContentControl?.IsKeyboardFocusWithin == true) //Only persistent the one with keyboard focus
+ {
+ var element = Keyboard.FocusedElement;
+ _lastFocusedDialogElement = element;
+ }
+ else if (wParamLong == SC_RESTORE) //Restore
+ {
+ // Kinda hacky, but without a delay the focus doesn't always get set correctly because the Focus() method fires too early
+ Task.Delay(50).ContinueWith(_ => this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() =>
+ {
+ if (_lastFocusedDialogElement is UIElement { Focusable: true, IsVisible: true })
+ {
+ _lastFocusedDialogElement.Focus();
+ _lastFocusedDialogElement = null;
+ }
+ })));
+ }
+ break;
+ }
+ return IntPtr.Zero;
+ }
+ }
+
///
/// Returns a DialogSession for the currently open dialog for managing it programmatically. If no dialog is open, CurrentSession will return null
///
diff --git a/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml b/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml
new file mode 100644
index 0000000000..53ec2a6f53
--- /dev/null
+++ b/tests/MaterialDesignThemes.UITests/Samples/DialogHost/WithMultipleTextBoxes.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
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 088c82ad1a..a19f7bb536 100644
--- a/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs
+++ b/tests/MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs
@@ -314,7 +314,6 @@ public async Task CornerRadius_AppliedToContentCoverBorder_WhenSetOnEmbeddedDial
await Wait.For(async () =>
{
var contentCoverBorder = await dialogHost.GetElement("ContentCoverBorder");
-
await Assert.That((await contentCoverBorder.GetCornerRadius()).TopLeft).IsEqualTo(1);
await Assert.That((await contentCoverBorder.GetCornerRadius()).TopRight).IsEqualTo(2);
await Assert.That((await contentCoverBorder.GetCornerRadius()).BottomRight).IsEqualTo(3);
@@ -500,7 +499,6 @@ public async Task DialogHost_WithComboBox_CanSelectItem()
var comboBox = await dialogHost.GetElement("TargetedPlatformComboBox");
await Task.Delay(500, TestContext.Current!.CancellationToken);
await comboBox.LeftClick();
-
var item = await Wait.For(() => comboBox.GetElement("TargetItem"));
await Task.Delay(TimeSpan.FromSeconds(1));
await item.LeftClick();
@@ -514,4 +512,55 @@ await Wait.For(async () =>
recorder.Success();
}
+
+ [Test]
+ [Description("Issue 3434")]
+ [Arguments(WindowState.Minimized, WindowState.Maximized, null)]
+ [Arguments(WindowState.Minimized, WindowState.Normal, null)]
+ [Arguments(WindowState.Maximized, WindowState.Normal, null)]
+ [Arguments(WindowState.Minimized, WindowState.Maximized, "MaterialDesignEmbeddedDialogHost")]
+ [Arguments(WindowState.Minimized, WindowState.Normal, "MaterialDesignEmbeddedDialogHost")]
+ [Arguments(WindowState.Maximized, 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, TestContext.Current!.CancellationToken);
+
+ // 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, TestContext.Current!.CancellationToken);
+ // Second state
+ await dialogHost.RemoteExecute(SetStateOfParentWindow, secondWindowState);
+ await Task.Delay(400, TestContext.Current!.CancellationToken);
+
+ // After changing state of the window the previously focused element should be focused again
+ await Assert.That(await tbTwo.GetIsFocused()).IsTrue();
+ recorder.Success();
+
+ static object SetStateOfParentWindow(DialogHost dialogHost, WindowState state)
+ {
+ var window = Window.GetWindow(dialogHost);
+ window.WindowState = state;
+ return null!;
+ }
+ static object SetDialogHostStyle(DialogHost dialogHost, string styleName)
+ {
+ Style style = (Style)dialogHost.FindResource(styleName);
+ dialogHost.Style = style;
+ return null!;
+ }
+ }
}