Skip to content

Commit b53fbd8

Browse files
Properly restore focus when TabControl.SelectedIndex is set via DialogHost (#3104)
* Add sample project reproducing the issue * "Hacky fix" to showcase what the root cause is * Remove hacky fix * Add failing UI test * Revert sample project commit; now replaced by UI test * Add RestoreFocusElement DP to make test run green * Add failing UI test for "disable" scenario * Add DialogHost.IsFocusRestoreDisabled DP to opt. disable focus restore * Rename DP to DialogHost.IsRestoreFocusDisabled * Add UI tests for navigation rail * Region rephrase
1 parent 22b8b49 commit b53fbd8

File tree

7 files changed

+323
-1
lines changed

7 files changed

+323
-1
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.DialogHost.RestoreFocus"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:local="clr-namespace:MaterialDesignThemes.UITests.Samples.DialogHost"
7+
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
8+
mc:Ignorable="d"
9+
d:DataContext="{d:DesignInstance Type=local:RestoreFocus}"
10+
d:DesignHeight="450" d:DesignWidth="800">
11+
<Grid>
12+
<materialDesign:DialogHost x:Name="DialogHost">
13+
<Grid>
14+
<Grid.RowDefinitions>
15+
<RowDefinition Height="Auto" />
16+
<RowDefinition Height="*" />
17+
<RowDefinition Height="*" />
18+
</Grid.RowDefinitions>
19+
20+
<Menu x:Name="Menu" Grid.Row="0" HorizontalAlignment="Right">
21+
<MenuItem x:Name="MenuItem1">
22+
<MenuItem.Header>
23+
<StackPanel Orientation="Horizontal">
24+
<materialDesign:PackIcon Kind="User" Height="20" Width="20" Margin="0,0,5,0" />
25+
<TextBlock Text="Menu" VerticalAlignment="Center" FontSize="16" />
26+
</StackPanel>
27+
</MenuItem.Header>
28+
<MenuItem x:Name="MenuItem2" Header="Settings" Icon="{materialDesign:PackIcon Kind=Settings}" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}" />
29+
</MenuItem>
30+
</Menu>
31+
32+
<TabControl x:Name="TabControl" Grid.Row="1">
33+
<TabItem x:Name="TabItem1" Header="Tab 1">
34+
<TextBlock Text="Page 1" />
35+
</TabItem>
36+
<TabItem x:Name="TabItem2" Header="Tab 2">
37+
<TextBlock Text="Page 2" />
38+
</TabItem>
39+
</TabControl>
40+
41+
<TabControl x:Name="NavigationRail" Grid.Row="2" Style="{StaticResource MaterialDesignNavigationRailTabControl}">
42+
<TabItem x:Name="RailItem1" Header="Rail 1">
43+
<TextBlock Text="Rail 1 content" />
44+
</TabItem>
45+
<TabItem x:Name="RailItem2" Header="Rail 2">
46+
<TextBlock Text="Rail 2 content" />
47+
</TabItem>
48+
</TabControl>
49+
</Grid>
50+
51+
<materialDesign:DialogHost.DialogContent>
52+
<Grid Margin="50">
53+
<Button x:Name="NavigateHomeButton" Content="Navigate to Tab 1" Click="NavigateHomeButton_OnClick" />
54+
</Grid>
55+
</materialDesign:DialogHost.DialogContent>
56+
</materialDesign:DialogHost>
57+
</Grid>
58+
</UserControl>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace MaterialDesignThemes.UITests.Samples.DialogHost;
2+
3+
public partial class RestoreFocus : UserControl
4+
{
5+
public RestoreFocus()
6+
{
7+
InitializeComponent();
8+
}
9+
10+
private void NavigateHomeButton_OnClick(object sender, RoutedEventArgs e)
11+
{
12+
Wpf.DialogHost.CloseDialogCommand.Execute(null, null);
13+
NavigationRail.SelectedItem = RailItem1;
14+
TabControl.SelectedItem = TabItem1;
15+
}
16+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<UserControl x:Class="MaterialDesignThemes.UITests.Samples.DialogHost.RestoreFocusDisabled"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:local="clr-namespace:MaterialDesignThemes.UITests.Samples.DialogHost"
7+
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
8+
mc:Ignorable="d"
9+
d:DataContext="{d:DesignInstance Type=local:RestoreFocus}"
10+
d:DesignHeight="450" d:DesignWidth="800">
11+
<UserControl.Resources>
12+
<!-- NOTE: Disable the DialogHost.RestoreFocusElement setters in the TabItem styles to avoid false positives -->
13+
<Style TargetType="TabItem" BasedOn="{StaticResource {x:Type TabItem}}">
14+
<Setter Property="materialDesign:DialogHost.RestoreFocusElement" Value="{x:Null}" />
15+
</Style>
16+
<Style x:Key="MaterialDesignNavigationRailTabItemWithRestoreFocusOverrideDisabled" TargetType="TabItem" BasedOn="{StaticResource MaterialDesignNavigationRailTabItem}">
17+
<Setter Property="materialDesign:DialogHost.RestoreFocusElement" Value="{x:Null}" />
18+
</Style>
19+
</UserControl.Resources>
20+
21+
<Grid>
22+
<materialDesign:DialogHost x:Name="DialogHost" IsRestoreFocusDisabled="True">
23+
<Grid>
24+
<Grid.RowDefinitions>
25+
<RowDefinition Height="Auto" />
26+
<RowDefinition Height="*" />
27+
<RowDefinition Height="*" />
28+
</Grid.RowDefinitions>
29+
30+
<Menu x:Name="Menu" Grid.Row="0" HorizontalAlignment="Right">
31+
<MenuItem x:Name="MenuItem1">
32+
<MenuItem.Header>
33+
<StackPanel Orientation="Horizontal">
34+
<materialDesign:PackIcon Kind="User" Height="20" Width="20" Margin="0,0,5,0" />
35+
<TextBlock Text="Menu" VerticalAlignment="Center" FontSize="16" />
36+
</StackPanel>
37+
</MenuItem.Header>
38+
<MenuItem x:Name="MenuItem2" Header="Settings" Icon="{materialDesign:PackIcon Kind=Settings}" Command="{x:Static materialDesign:DialogHost.OpenDialogCommand}" />
39+
</MenuItem>
40+
</Menu>
41+
42+
<TabControl x:Name="TabControl" Grid.Row="1">
43+
<TabItem x:Name="TabItem1" Header="Tab 1">
44+
<TextBlock Text="Page 1" />
45+
</TabItem>
46+
<TabItem x:Name="TabItem2" Header="Tab 2">
47+
<TextBlock Text="Page 2" />
48+
</TabItem>
49+
</TabControl>
50+
51+
<TabControl x:Name="NavigationRail" Grid.Row="2" Style="{StaticResource MaterialDesignNavigationRailTabControl}" ItemContainerStyle="{StaticResource MaterialDesignNavigationRailTabItemWithRestoreFocusOverrideDisabled}">
52+
<TabItem x:Name="RailItem1" Header="Rail 1">
53+
<TextBlock Text="Rail 1 content" />
54+
</TabItem>
55+
<TabItem x:Name="RailItem2" Header="Rail 2">
56+
<TextBlock Text="Rail 2 content" />
57+
</TabItem>
58+
</TabControl>
59+
</Grid>
60+
61+
<materialDesign:DialogHost.DialogContent>
62+
<Grid Margin="50">
63+
<Button x:Name="NavigateHomeButton" Content="Navigate to Tab 1" Click="NavigateHomeButton_OnClick" />
64+
</Grid>
65+
</materialDesign:DialogHost.DialogContent>
66+
</materialDesign:DialogHost>
67+
</Grid>
68+
</UserControl>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace MaterialDesignThemes.UITests.Samples.DialogHost;
2+
3+
public partial class RestoreFocusDisabled : UserControl
4+
{
5+
public RestoreFocusDisabled()
6+
{
7+
InitializeComponent();
8+
}
9+
10+
private void NavigateHomeButton_OnClick(object sender, RoutedEventArgs e)
11+
{
12+
Wpf.DialogHost.CloseDialogCommand.Execute(null, null);
13+
NavigationRail.SelectedItem = RailItem1;
14+
TabControl.SelectedItem = TabItem1;
15+
}
16+
}

MaterialDesignThemes.UITests/WPF/DialogHosts/DialogHostTests.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,5 +349,135 @@ await Wait.For(async () =>
349349
await closeButton.LeftClick();
350350
Assert.False(await dialogHost.GetIsOpen());
351351
});
352+
353+
recorder.Success();
354+
}
355+
356+
[Fact]
357+
[Description("Issue 3094")]
358+
public async Task DialogHost_ChangesSelectedTabItem_DoesNotPerformTabChangeWhenRestoringFocus()
359+
{
360+
await using var recorder = new TestRecorder(App);
361+
362+
IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocus>()).As<Grid>();
363+
IVisualElement<TabItem> tabItem1 = await rootGrid.GetElement<TabItem>("TabItem1");
364+
IVisualElement<TabItem> tabItem2 = await rootGrid.GetElement<TabItem>("TabItem2");
365+
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");
366+
367+
// Select TabItem2
368+
await tabItem2.LeftClick();
369+
370+
// Open menu
371+
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
372+
await menuItem1.LeftClick();
373+
await Task.Delay(1000); // Wait for menu to open
374+
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
375+
await menuItem2.LeftClick();
376+
await Task.Delay(1000); // Wait for dialog content to show
377+
378+
// Click navigate button
379+
await navigateHomeButton.LeftClick();
380+
await Task.Delay(1000); // Wait for dialog content to close
381+
382+
Assert.True(await tabItem1.GetIsSelected());
383+
Assert.False(await tabItem2.GetIsSelected());
384+
385+
recorder.Success();
386+
}
387+
388+
[Fact]
389+
[Description("Issue 3094")]
390+
public async Task DialogHost_ChangesSelectedRailItem_DoesNotPerformRailChangeWhenRestoringFocus()
391+
{
392+
await using var recorder = new TestRecorder(App);
393+
394+
IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocus>()).As<Grid>();
395+
IVisualElement<TabItem> railItem1 = await rootGrid.GetElement<TabItem>("RailItem1");
396+
IVisualElement<TabItem> railItem2 = await rootGrid.GetElement<TabItem>("RailItem2");
397+
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");
398+
399+
// Select TabItem2
400+
await railItem2.LeftClick();
401+
402+
// Open menu
403+
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
404+
await menuItem1.LeftClick();
405+
await Task.Delay(1000); // Wait for menu to open
406+
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
407+
await menuItem2.LeftClick();
408+
await Task.Delay(1000); // Wait for dialog content to show
409+
410+
// Click navigate button
411+
await navigateHomeButton.LeftClick();
412+
await Task.Delay(1000); // Wait for dialog content to close
413+
414+
Assert.True(await railItem1.GetIsSelected());
415+
Assert.False(await railItem2.GetIsSelected());
416+
417+
recorder.Success();
418+
}
419+
420+
[Fact]
421+
[Description("Issue 3094")]
422+
public async Task DialogHost_ChangesSelectedTabItem_DoesNotPerformTabChangeWhenRestoreFocusIsDisabled()
423+
{
424+
await using var recorder = new TestRecorder(App);
425+
426+
IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocusDisabled>()).As<Grid>();
427+
IVisualElement<TabItem> tabItem1 = await rootGrid.GetElement<TabItem>("TabItem1");
428+
IVisualElement<TabItem> tabItem2 = await rootGrid.GetElement<TabItem>("TabItem2");
429+
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");
430+
431+
// Select TabItem2
432+
await tabItem2.LeftClick();
433+
434+
// Open menu
435+
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
436+
await menuItem1.LeftClick();
437+
await Task.Delay(1000); // Wait for menu to open
438+
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
439+
await menuItem2.LeftClick();
440+
await Task.Delay(1000); // Wait for dialog content to show
441+
442+
// Click navigate button
443+
await navigateHomeButton.LeftClick();
444+
await Task.Delay(1000); // Wait for dialog content to close
445+
446+
Assert.True(await tabItem1.GetIsSelected());
447+
Assert.False(await tabItem2.GetIsSelected());
448+
449+
recorder.Success();
450+
}
451+
452+
[Fact]
453+
[Description("Issue 3094")]
454+
public async Task DialogHost_ChangesSelectedRailItem_DoesNotPerformRailChangeWhenRestoreFocusIsDisabled()
455+
{
456+
await using var recorder = new TestRecorder(App);
457+
458+
IVisualElement<Grid> rootGrid = (await LoadUserControl<RestoreFocusDisabled>()).As<Grid>();
459+
IVisualElement<TabItem> railItem1 = await rootGrid.GetElement<TabItem>("RailItem1");
460+
IVisualElement<TabItem> railItem2 = await rootGrid.GetElement<TabItem>("RailItem2");
461+
IVisualElement<Button> navigateHomeButton = await rootGrid.GetElement<Button>("NavigateHomeButton");
462+
463+
// Select TabItem2
464+
await railItem2.LeftClick();
465+
466+
// Open menu
467+
IVisualElement<MenuItem> menuItem1 = await rootGrid.GetElement<MenuItem>("MenuItem1");
468+
await menuItem1.LeftClick();
469+
await Task.Delay(1000); // Wait for menu to open
470+
IVisualElement<MenuItem> menuItem2 = await rootGrid.GetElement<MenuItem>("MenuItem2");
471+
await menuItem2.LeftClick();
472+
await Task.Delay(1000); // Wait for dialog content to show
473+
474+
// Click navigate button
475+
await navigateHomeButton.LeftClick();
476+
await Task.Delay(1000); // Wait for dialog content to close
477+
478+
Assert.True(await railItem1.GetIsSelected());
479+
Assert.False(await railItem2.GetIsSelected());
480+
481+
recorder.Success();
352482
}
353483
}

MaterialDesignThemes.Wpf/DialogHost.cs

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,17 @@ private static void IsOpenPropertyChangedCallback(DependencyObject dependencyObj
367367

368368
dialogHost.CurrentSession = new DialogSession(dialogHost);
369369
var window = Window.GetWindow(dialogHost);
370-
dialogHost._restoreFocusDialogClose = window != null ? FocusManager.GetFocusedElement(window) : null;
370+
if (!dialogHost.IsRestoreFocusDisabled)
371+
{
372+
dialogHost._restoreFocusDialogClose = window != null ? FocusManager.GetFocusedElement(window) : null;
373+
374+
// Check restore focus override
375+
if (dialogHost._restoreFocusDialogClose is DependencyObject dependencyObj &&
376+
GetRestoreFocusElement(dependencyObj) is { } focusOverride)
377+
{
378+
dialogHost._restoreFocusDialogClose = focusOverride;
379+
}
380+
}
371381

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

615+
#region restore focus properties
616+
617+
public static readonly DependencyProperty RestoreFocusElementProperty = DependencyProperty.RegisterAttached(
618+
"RestoreFocusElement", typeof(IInputElement), typeof(DialogHost), new PropertyMetadata(default(IInputElement)));
619+
620+
public static void SetRestoreFocusElement(DependencyObject element, IInputElement value)
621+
=> element.SetValue(RestoreFocusElementProperty, value);
622+
623+
public static IInputElement GetRestoreFocusElement(DependencyObject element)
624+
=> (IInputElement) element.GetValue(RestoreFocusElementProperty);
625+
626+
public static readonly DependencyProperty IsRestoreFocusDisabledProperty = DependencyProperty.Register(
627+
nameof(IsRestoreFocusDisabled), typeof(bool), typeof(DialogHost), new PropertyMetadata(false));
628+
629+
public bool IsRestoreFocusDisabled
630+
{
631+
get => (bool) GetValue(IsRestoreFocusDisabledProperty);
632+
set => SetValue(IsRestoreFocusDisabledProperty, value);
633+
}
634+
635+
#endregion
636+
605637
#region open dialog events/callbacks
606638

607639
public static readonly RoutedEvent DialogOpenedEvent =

MaterialDesignThemes.Wpf/Themes/MaterialDesignTheme.TabControl.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@
249249
<Setter Property="Height" Value="48" />
250250
<Setter Property="MinWidth" Value="90" />
251251
<Setter Property="Padding" Value="16,12" />
252+
<Setter Property="wpf:DialogHost.RestoreFocusElement" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TabControl}}" />
252253
<Setter Property="Template">
253254
<Setter.Value>
254255
<ControlTemplate TargetType="{x:Type TabItem}">
@@ -483,6 +484,7 @@
483484
<Setter Property="Foreground" Value="{DynamicResource MaterialDesignBody}" />
484485
<Setter Property="Height" Value="72" />
485486
<Setter Property="Padding" Value="0" />
487+
<Setter Property="wpf:DialogHost.RestoreFocusElement" Value="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TabControl}}" />
486488
<Setter Property="Template">
487489
<Setter.Value>
488490
<ControlTemplate TargetType="{x:Type TabItem}">

0 commit comments

Comments
 (0)