Skip to content

Commit 5881263

Browse files
Fix PasswordBox keyboard focus traversal issue (#3096)
* Extend SmartHint demo page with new features * Handle forward/backward keyboard focus on reveal style PasswordBox * Add UI test to verify tabbing works * Fix issue with backwards focus traversal when hiding a revealed password * Update UI test with syntax feature which is hopefully added to XAMLTest * Rev'ing XAMLTest --------- Co-authored-by: Kevin Bost <[email protected]>
1 parent 4590c59 commit 5881263

File tree

3 files changed

+76
-5
lines changed

3 files changed

+76
-5
lines changed

MainDemo.Wpf/SmartHint.xaml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -408,7 +408,11 @@
408408
</Grid>
409409

410410
<!-- Reveal style PasswordBox variants -->
411-
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="PasswordBox 'reveal' styles" Margin="0,40,0,0" />
411+
<StackPanel Orientation="Horizontal" Margin="0,40,0,0">
412+
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="PasswordBox 'reveal' styles" />
413+
<CheckBox x:Name="PasswordBoxesRevealedCheckBox" Content="IsPasswordRevealed" Margin="20,0,0,0" />
414+
</StackPanel>
415+
412416
<Grid>
413417
<Grid.Resources>
414418
<Style TargetType="{x:Type PasswordBox}" BasedOn="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}">
@@ -417,6 +421,7 @@
417421
<Setter Property="materialDesign:TextFieldAssist.LeadingIcon" Value="{StaticResource LeadingIcon}" />
418422
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
419423
<Setter Property="materialDesign:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
424+
<Setter Property="materialDesign:PasswordBoxAssist.IsPasswordRevealed" Value="{Binding ElementName=PasswordBoxesRevealedCheckBox, Path=IsChecked}" />
420425
<Setter Property="Padding">
421426
<Setter.Value>
422427
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
@@ -458,6 +463,7 @@
458463
<Setter Property="materialDesign:TextFieldAssist.LeadingIcon" Value="{StaticResource LeadingIcon}" />
459464
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
460465
<Setter Property="materialDesign:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
466+
<Setter Property="materialDesign:PasswordBoxAssist.IsPasswordRevealed" Value="{Binding ElementName=PasswordBoxesRevealedCheckBox, Path=IsChecked}" />
461467
<Setter Property="Padding">
462468
<Setter.Value>
463469
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
@@ -499,6 +505,7 @@
499505
<Setter Property="materialDesign:TextFieldAssist.LeadingIcon" Value="{StaticResource LeadingIcon}" />
500506
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
501507
<Setter Property="materialDesign:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
508+
<Setter Property="materialDesign:PasswordBoxAssist.IsPasswordRevealed" Value="{Binding ElementName=PasswordBoxesRevealedCheckBox, Path=IsChecked}" />
502509
<Setter Property="Padding">
503510
<Setter.Value>
504511
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
@@ -534,7 +541,10 @@
534541
</Grid>
535542

536543
<!-- ComboBox variants -->
537-
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="ComboBox styles" Margin="0,40,0,0" />
544+
<StackPanel Orientation="Horizontal" Margin="0,40,0,0">
545+
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="ComboBox styles" />
546+
<CheckBox x:Name="ComboBoxesEditableCheckBox" Content="IsEditable" Margin="20,0,0,0" />
547+
</StackPanel>
538548
<Grid>
539549
<Grid.Resources>
540550
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource MaterialDesignFloatingHintComboBox}">
@@ -544,7 +554,7 @@
544554
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
545555
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
546556
<Setter Property="ItemsSource" Value="{Binding ComboBoxOptions}" />
547-
<Setter Property="IsEditable" Value="True" />
557+
<Setter Property="IsEditable" Value="{Binding ElementName=ComboBoxesEditableCheckBox, Path=IsChecked}" />
548558
<Setter Property="Padding">
549559
<Setter.Value>
550560
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
@@ -587,7 +597,7 @@
587597
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
588598
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
589599
<Setter Property="ItemsSource" Value="{Binding ComboBoxOptions}" />
590-
<Setter Property="IsEditable" Value="True" />
600+
<Setter Property="IsEditable" Value="{Binding ElementName=ComboBoxesEditableCheckBox, Path=IsChecked}" />
591601
<Setter Property="Padding">
592602
<Setter.Value>
593603
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
@@ -630,7 +640,7 @@
630640
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
631641
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
632642
<Setter Property="ItemsSource" Value="{Binding ComboBoxOptions}" />
633-
<Setter Property="IsEditable" Value="True" />
643+
<Setter Property="IsEditable" Value="{Binding ElementName=ComboBoxesEditableCheckBox, Path=IsChecked}" />
634644
<Setter Property="Padding">
635645
<Setter.Value>
636646
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">

MaterialDesignThemes.UITests/WPF/PasswordBoxes/PasswordBoxTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,41 @@ public async Task PasswordBox_WithHintAndValidationError_RespectsPadding(string
296296

297297
recorder.Success();
298298
}
299+
300+
[Fact]
301+
[Description("Issue 3095")]
302+
public async Task PasswordBox_WithRevealedPassword_RespectsKeyboardTabNavigation()
303+
{
304+
await using var recorder = new TestRecorder(App);
305+
306+
var stackPanel = await LoadXaml<StackPanel>(@"
307+
<StackPanel Orientation=""Vertical"">
308+
<TextBox x:Name=""TextBox1"" Width=""200"" />
309+
<PasswordBox x:Name=""PasswordBox"" Width=""200""
310+
materialDesign:PasswordBoxAssist.IsPasswordRevealed=""True""
311+
Style=""{StaticResource MaterialDesignFloatingHintRevealPasswordBox}"" />
312+
<TextBox x:Name=""TextBox2"" Width=""200"" />
313+
</StackPanel>");
314+
315+
var textBox1 = await stackPanel.GetElement<TextBox>("TextBox1");
316+
var passwordBox = await stackPanel.GetElement<PasswordBox>("PasswordBox");
317+
var revealPasswordTextBox = await passwordBox.GetElement<TextBox>("RevealPasswordTextBox");
318+
var textBox2 = await stackPanel.GetElement<TextBox>("TextBox2");
319+
320+
// Assert Tab forward
321+
await textBox1.MoveKeyboardFocus();
322+
Assert.True(await textBox1.GetIsKeyboardFocused());
323+
await textBox1.SendKeyboardInput($"{Key.Tab}");
324+
Assert.True(await revealPasswordTextBox.GetIsKeyboardFocused());
325+
await revealPasswordTextBox.SendKeyboardInput($"{Key.Tab}");
326+
Assert.True(await textBox2.GetIsKeyboardFocused());
327+
328+
// Assert Tab backwards
329+
await textBox2.SendKeyboardInput($"{ModifierKeys.Shift}{Key.Tab}");
330+
Assert.True(await revealPasswordTextBox.GetIsKeyboardFocused());
331+
await revealPasswordTextBox.SendKeyboardInput($"{Key.Tab}{ModifierKeys.None}");
332+
Assert.True(await textBox1.GetIsKeyboardFocused());
333+
334+
recorder.Success();
335+
}
299336
}

MaterialDesignThemes.Wpf/Behaviors/PasswordBoxBehavior.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,41 @@ internal class PasswordBoxBehavior : Behavior<PasswordBox>
66
{
77
private void PasswordBoxLoaded(object sender, RoutedEventArgs e) => PasswordBoxAssist.SetPassword(AssociatedObject, AssociatedObject.Password);
88

9+
private void PasswordBoxPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
10+
{
11+
if (PasswordBoxAssist.GetIsPasswordRevealed(AssociatedObject) &&
12+
AssociatedObject.FindChild<TextBox>("RevealPasswordTextBox") is { } revealPasswordTextBox)
13+
{
14+
if (ReferenceEquals(e.OldFocus, revealPasswordTextBox) && ReferenceEquals(e.NewFocus, AssociatedObject))
15+
{
16+
// When password box receives keyboard focus, but it came from the nested reveal TextBox. We request focus transfer to the previous element from the password box's POV
17+
TraversalRequest request = new TraversalRequest(FocusNavigationDirection.Previous);
18+
AssociatedObject.MoveFocus(request);
19+
e.Handled = true;
20+
}
21+
else if (!ReferenceEquals(e.OriginalSource, revealPasswordTextBox))
22+
{
23+
// When password box receives keyboard focus while the password is revealed, we transfer the focus to the nested reveal TextBox.
24+
revealPasswordTextBox.Focus();
25+
e.Handled = true;
26+
}
27+
}
28+
29+
}
30+
931
protected override void OnAttached()
1032
{
1133
base.OnAttached();
1234
AssociatedObject.Loaded += PasswordBoxLoaded;
35+
AssociatedObject.PreviewGotKeyboardFocus += PasswordBoxPreviewGotKeyboardFocus;
1336
}
1437

1538
protected override void OnDetaching()
1639
{
1740
if (AssociatedObject != null)
1841
{
1942
AssociatedObject.Loaded -= PasswordBoxLoaded;
43+
AssociatedObject.PreviewGotKeyboardFocus -= PasswordBoxPreviewGotKeyboardFocus;
2044
}
2145
base.OnDetaching();
2246
}

0 commit comments

Comments
 (0)