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
48 changes: 48 additions & 0 deletions src/MaterialDesignThemes.Wpf/DialogHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<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: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 @@ -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<WithMultipleTextBoxes>()).As<DialogHost>();
if (styleName is not null)
{
await dialogHost.RemoteExecute(SetDialogHostStyle, styleName);
}
await Task.Delay(400);

// 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);
// 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<Grid>("""
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox x:Name="OutsideTextBox" VerticalAlignment="Center" />
<materialDesign:DialogHost x:Name="DialogHost" Grid.Column="1" IsOpen="True">
<materialDesign:DialogHost.DialogContent>
<TextBox x:Name="InsideTextBox" MinWidth="200" />
</materialDesign:DialogHost.DialogContent>
</materialDesign:DialogHost>
</Grid>
""");
var dialogHost = await grid.GetElement<DialogHost>("DialogHost");
if (styleName is not null)
{
await dialogHost.RemoteExecute(SetDialogHostStyle, styleName);
}
await Task.Delay(400);

var insideTextBox = await dialogHost.GetElement<TextBox>("InsideTextBox");
var outsideTextBox = await grid.GetElement<TextBox>("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);
Expand Down
Loading