Skip to content
Merged
58 changes: 58 additions & 0 deletions MaterialDesignThemes.UITests/Samples/DialogHost/RestoreFocus.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.DialogHost.RestoreFocus"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:RestoreFocus}"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<materialDesign:DialogHost x:Name="DialogHost">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Menu x:Name="Menu" Grid.Row="0" HorizontalAlignment="Right">
<MenuItem x:Name="MenuItem1">
<MenuItem.Header>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="User" Height="20" Width="20" Margin="0,0,5,0" />
<TextBlock Text="Menu" VerticalAlignment="Center" FontSize="16" />
</StackPanel>
</MenuItem.Header>
<MenuItem x:Name="MenuItem2" Header="Settings" Icon="{materialDesign:PackIcon Kind=Settings}" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}" />
</MenuItem>
</Menu>

<TabControl x:Name="TabControl" Grid.Row="1">
<TabItem x:Name="TabItem1" Header="Tab 1">
<TextBlock Text="Page 1" />
</TabItem>
<TabItem x:Name="TabItem2" Header="Tab 2">
<TextBlock Text="Page 2" />
</TabItem>
</TabControl>

<TabControl x:Name="NavigationRail" Grid.Row="2" Style="{StaticResource MaterialDesignNavigationRailTabControl}">
<TabItem x:Name="RailItem1" Header="Rail 1">
<TextBlock Text="Rail 1 content" />
</TabItem>
<TabItem x:Name="RailItem2" Header="Rail 2">
<TextBlock Text="Rail 2 content" />
</TabItem>
</TabControl>
</Grid>

<materialDesign:DialogHost.DialogContent>
<Grid Margin="50">
<Button x:Name="NavigateHomeButton" Content="Navigate to Tab 1" Click="NavigateHomeButton_OnClick" />
</Grid>
</materialDesign:DialogHost.DialogContent>
</materialDesign:DialogHost>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace MaterialDesignThemes.UITests.Samples.DialogHost;

public partial class RestoreFocus : UserControl
{
public RestoreFocus()
{
InitializeComponent();
}

private void NavigateHomeButton_OnClick(object sender, RoutedEventArgs e)
{
Wpf.DialogHost.CloseDialogCommand.Execute(null, null);
NavigationRail.SelectedItem = RailItem1;
TabControl.SelectedItem = TabItem1;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.DialogHost.RestoreFocusDisabled"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=local:RestoreFocus}"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<!-- NOTE: Disable the DialogHost.RestoreFocusElement setters in the TabItem styles to avoid false positives -->
<Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
<Setter Property="materialDesign:DialogHost.RestoreFocusElement" Value="{x:Null}" />
</Style>
<Style x:Key="MaterialDesignNavigationRailTabItemWithRestoreFocusOverrideDisabled" TargetType="TabItem" BasedOn="{StaticResource MaterialDesignNavigationRailTabItem}">
<Setter Property="materialDesign:DialogHost.RestoreFocusElement" Value="{x:Null}" />
</Style>
</UserControl.Resources>

<Grid>
<materialDesign:DialogHost x:Name="DialogHost" IsRestoreFocusDisabled="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>

<Menu x:Name="Menu" Grid.Row="0" HorizontalAlignment="Right">
<MenuItem x:Name="MenuItem1">
<MenuItem.Header>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="User" Height="20" Width="20" Margin="0,0,5,0" />
<TextBlock Text="Menu" VerticalAlignment="Center" FontSize="16" />
</StackPanel>
</MenuItem.Header>
<MenuItem x:Name="MenuItem2" Header="Settings" Icon="{materialDesign:PackIcon Kind=Settings}" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}" />
</MenuItem>
</Menu>

<TabControl x:Name="TabControl" Grid.Row="1">
<TabItem x:Name="TabItem1" Header="Tab 1">
<TextBlock Text="Page 1" />
</TabItem>
<TabItem x:Name="TabItem2" Header="Tab 2">
<TextBlock Text="Page 2" />
</TabItem>
</TabControl>

<TabControl x:Name="NavigationRail" Grid.Row="2" Style="{StaticResource MaterialDesignNavigationRailTabControl}" ItemContainerStyle="{StaticResource MaterialDesignNavigationRailTabItemWithRestoreFocusOverrideDisabled}">
<TabItem x:Name="RailItem1" Header="Rail 1">
<TextBlock Text="Rail 1 content" />
</TabItem>
<TabItem x:Name="RailItem2" Header="Rail 2">
<TextBlock Text="Rail 2 content" />
</TabItem>
</TabControl>
</Grid>

<materialDesign:DialogHost.DialogContent>
<Grid Margin="50">
<Button x:Name="NavigateHomeButton" Content="Navigate to Tab 1" Click="NavigateHomeButton_OnClick" />
</Grid>
</materialDesign:DialogHost.DialogContent>
</materialDesign:DialogHost>
</Grid>
</UserControl>
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace MaterialDesignThemes.UITests.Samples.DialogHost;

public partial class RestoreFocusDisabled : UserControl
{
public RestoreFocusDisabled()
{
InitializeComponent();
}

private void NavigateHomeButton_OnClick(object sender, RoutedEventArgs e)
{
Wpf.DialogHost.CloseDialogCommand.Execute(null, null);
NavigationRail.SelectedItem = RailItem1;
TabControl.SelectedItem = TabItem1;
}
}
130 changes: 130 additions & 0 deletions MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,5 +349,135 @@ await Wait.For(async () =>
await closeButton.LeftClick();
Assert.False(await dialogHost.GetIsOpen());
});

recorder.Success();
}

[Fact]
[Description("Issue 3094")]
public async Task DialogHost_ChangesSelectedTabItem_DoesNotPerformTabChangeWhenRestoringFocus()
{
await using var recorder = new TestRecorder(App);

IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocus>()).As<Grid>();
IVisualElement<TabItem> tabItem1 = await rootGrid.GetElement<TabItem>("TabItem1");
IVisualElement<TabItem> tabItem2 = await rootGrid.GetElement<TabItem>("TabItem2");
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");

// Select TabItem2
await tabItem2.LeftClick();

// Open menu
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
await menuItem1.LeftClick();
await Task.Delay(1000); // Wait for menu to open
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
await menuItem2.LeftClick();
await Task.Delay(1000); // Wait for dialog content to show

// Click navigate button
await navigateHomeButton.LeftClick();
await Task.Delay(1000); // Wait for dialog content to close

Assert.True(await tabItem1.GetIsSelected());
Assert.False(await tabItem2.GetIsSelected());

recorder.Success();
}

[Fact]
[Description("Issue 3094")]
public async Task DialogHost_ChangesSelectedRailItem_DoesNotPerformRailChangeWhenRestoringFocus()
{
await using var recorder = new TestRecorder(App);

IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocus>()).As<Grid>();
IVisualElement<TabItem> railItem1 = await rootGrid.GetElement<TabItem>("RailItem1");
IVisualElement<TabItem> railItem2 = await rootGrid.GetElement<TabItem>("RailItem2");
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");

// Select TabItem2
await railItem2.LeftClick();

// Open menu
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
await menuItem1.LeftClick();
await Task.Delay(1000); // Wait for menu to open
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
await menuItem2.LeftClick();
await Task.Delay(1000); // Wait for dialog content to show

// Click navigate button
await navigateHomeButton.LeftClick();
await Task.Delay(1000); // Wait for dialog content to close

Assert.True(await railItem1.GetIsSelected());
Assert.False(await railItem2.GetIsSelected());

recorder.Success();
}

[Fact]
[Description("Issue 3094")]
public async Task DialogHost_ChangesSelectedTabItem_DoesNotPerformTabChangeWhenRestoreFocusIsDisabled()
{
await using var recorder = new TestRecorder(App);

IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocusDisabled>()).As<Grid>();
IVisualElement<TabItem> tabItem1 = await rootGrid.GetElement<TabItem>("TabItem1");
IVisualElement<TabItem> tabItem2 = await rootGrid.GetElement<TabItem>("TabItem2");
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");

// Select TabItem2
await tabItem2.LeftClick();

// Open menu
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
await menuItem1.LeftClick();
await Task.Delay(1000); // Wait for menu to open
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
await menuItem2.LeftClick();
await Task.Delay(1000); // Wait for dialog content to show

// Click navigate button
await navigateHomeButton.LeftClick();
await Task.Delay(1000); // Wait for dialog content to close

Assert.True(await tabItem1.GetIsSelected());
Assert.False(await tabItem2.GetIsSelected());

recorder.Success();
}

[Fact]
[Description("Issue 3094")]
public async Task DialogHost_ChangesSelectedRailItem_DoesNotPerformRailChangeWhenRestoreFocusIsDisabled()
{
await using var recorder = new TestRecorder(App);

IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocusDisabled>()).As<Grid>();
IVisualElement<TabItem> railItem1 = await rootGrid.GetElement<TabItem>("RailItem1");
IVisualElement<TabItem> railItem2 = await rootGrid.GetElement<TabItem>("RailItem2");
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");

// Select TabItem2
await railItem2.LeftClick();

// Open menu
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
await menuItem1.LeftClick();
await Task.Delay(1000); // Wait for menu to open
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
await menuItem2.LeftClick();
await Task.Delay(1000); // Wait for dialog content to show

// Click navigate button
await navigateHomeButton.LeftClick();
await Task.Delay(1000); // Wait for dialog content to close

Assert.True(await railItem1.GetIsSelected());
Assert.False(await railItem2.GetIsSelected());

recorder.Success();
}
}
34 changes: 33 additions & 1 deletion MaterialDesignThemes.Wpf/DialogHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,17 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj

dialogHost.CurrentSession = new DialogSession(dialogHost);
var window = Window.GetWindow(dialogHost);
dialogHost._restoreFocusDialogClose = window != null ? FocusManager.GetFocusedElement(window) : null;
if (!dialogHost.IsRestoreFocusDisabled)
{
dialogHost._restoreFocusDialogClose = window != null ? FocusManager.GetFocusedElement(window) : null;

// Check restore focus override
if (dialogHost._restoreFocusDialogClose is DependencyObject dependencyObj &&
GetRestoreFocusElement(dependencyObj) is { } focusOverride)
{
dialogHost._restoreFocusDialogClose = focusOverride;
}
}

//multiple ways of calling back that the dialog has opened:
// * routed event
Expand Down Expand Up @@ -602,6 +612,28 @@ public override void OnApplyTemplate()
base.OnApplyTemplate();
}

#region restore focus properties

public static readonly DependencyProperty RestoreFocusElementProperty = DependencyProperty.RegisterAttached(
"RestoreFocusElement", typeof(IInputElement), typeof(DialogHost), new PropertyMetadata(default(IInputElement)));

public static void SetRestoreFocusElement(DependencyObject element, IInputElement value)
=> element.SetValue(RestoreFocusElementProperty, value);

public static IInputElement GetRestoreFocusElement(DependencyObject element)
=> (IInputElement) element.GetValue(RestoreFocusElementProperty);

public static readonly DependencyProperty IsRestoreFocusDisabledProperty = DependencyProperty.Register(
nameof(IsRestoreFocusDisabled), typeof(bool), typeof(DialogHost), new PropertyMetadata(false));

public bool IsRestoreFocusDisabled
{
get => (bool) GetValue(IsRestoreFocusDisabledProperty);
set => SetValue(IsRestoreFocusDisabledProperty, value);
}

#endregion

#region open dialog events/callbacks

public static readonly RoutedEvent DialogOpenedEvent =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
<Setter Property="Height" Value="48" />
<Setter Property="MinWidth" Value="90" />
<Setter Property="Padding" Value="16,12" />
<Setter Property="wpf:DialogHost.RestoreFocusElement" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TabControl}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
Expand Down Expand Up @@ -483,6 +484,7 @@
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignBody}" />
<Setter Property="Height" Value="72" />
<Setter Property="Padding" Value="0" />
<Setter Property="wpf:DialogHost.RestoreFocusElement" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TabControl}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
Expand Down