Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion src/MaterialDesignThemes.Wpf/DialogHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ public class DialogHost : ContentControl
private DialogClosingEventHandler? _attachedDialogClosingEventHandler;
private DialogClosedEventHandler? _attachedDialogClosedEventHandler;
private IInputElement? _restoreFocusDialogClose;
private IInputElement? _lastFocusedDialogElement;
private WindowState _previousWindowState;
private Action? _currentSnackbarMessageQueueUnPauseAction;

static DialogHost()
Expand Down Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -405,6 +409,53 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj
})));
}


private void ListenForWindowStateChanged(Window? window)
{

if (window is not null)
{
window.StateChanged -= Window_StateChanged;
window.StateChanged += Window_StateChanged;
}
}

private void Window_StateChanged(object? sender, EventArgs e)
{
if (sender is not Window window)
{
return;
}

var windowState = window.WindowState;
if (windowState == WindowState.Minimized)
{
_lastFocusedDialogElement = FocusManager.GetFocusedElement(window);
_previousWindowState = windowState;
return;
}

// We only need to focus anything manually if the window changes state from Minimized --> (Normal or Maximized)
// Going from Normal --> Maximized (and vice versa) is fine since the focus is already kept correctly
if (IsWindowRestoredFromMinimized())
{
// 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 (IsLastFocusedDialogElementFocusable())
{
_lastFocusedDialogElement!.Focus();
}
})));
}
_previousWindowState = windowState;

bool IsWindowRestoredFromMinimized() => (windowState == WindowState.Normal || windowState == WindowState.Maximized) &&
_previousWindowState == WindowState.Minimized;

bool IsLastFocusedDialogElementFocusable() => _lastFocusedDialogElement is UIElement { Focusable: true, IsVisible: true };
}

/// <summary>
/// Returns a DialogSession for the currently open dialog for managing it programmatically. If no dialog is open, CurrentSession will return null
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.DialogHost.WithMultipleTextBoxes"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:MaterialDesignThemes.UITests.Samples.DialogHost"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<materialDesign:DialogHost x:Name="SampleDialogHost" Loaded="DialogHost_Loaded">
<materialDesign:DialogHost.DialogContent>
<StackPanel Margin="16"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBox x:Name="TextBoxOne"
MinWidth="200"
Text="One" />
<TextBox x:Name="TextBoxTwo"
MinWidth="200"
Text="Two" />
<TextBox x:Name="TextBoxThree"
MinWidth="200"
Text="Three" />
</StackPanel>
</materialDesign:DialogHost.DialogContent>
</materialDesign:DialogHost>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace MaterialDesignThemes.UITests.Samples.DialogHost;

/// <summary>
/// Interaction logic for WithMultipleTextBoxes.xaml
/// </summary>
public partial class WithMultipleTextBoxes : UserControl
{
public WithMultipleTextBoxes()
{
InitializeComponent();
}
private void DialogHost_Loaded(object sender, RoutedEventArgs e)
{
SampleDialogHost.IsOpen = true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,6 @@ public async Task CornerRadius_AppliedToContentCoverBorder_WhenSetOnEmbeddedDial
await Wait.For(async () =>
{
var contentCoverBorder = await dialogHost.GetElement<Border>("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);
Expand Down Expand Up @@ -500,7 +499,6 @@ public async Task DialogHost_WithComboBox_CanSelectItem()
var comboBox = await dialogHost.GetElement<ComboBox>("TargetedPlatformComboBox");
await Task.Delay(500, TestContext.Current!.CancellationToken);
await comboBox.LeftClick();

var item = await Wait.For(() => comboBox.GetElement<ComboBoxItem>("TargetItem"));
await Task.Delay(TimeSpan.FromSeconds(1));
await item.LeftClick();
Expand All @@ -514,4 +512,40 @@ await Wait.For(async () =>

recorder.Success();
}

[Test]
[Description("Issue 3434")]
[Arguments(WindowState.Minimized, WindowState.Maximized)]
[Arguments(WindowState.Minimized, WindowState.Normal)]
[Arguments(WindowState.Maximized, WindowState.Normal)]
public async Task DialogHost_WhenWindowStateChanges_FocusedElementStaysFocused(WindowState firstWindowState, WindowState secondWindowState)
{
await using var recorder = new TestRecorder(App);

var dialogHost = (await LoadUserControl<WithMultipleTextBoxes>()).As<DialogHost>();
await Task.Delay(400, TestContext.Current!.CancellationToken);

// Select the second TextBox
var tbTwo = await dialogHost.GetElement<TextBox>("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!;
}
}
}
Loading