Skip to content

Commit

Permalink
Fix PasswordBox keyboard focus traversal issue (#3096)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
nicolaihenriksen and Keboo authored Mar 2, 2023
1 parent 4590c59 commit 5881263
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 5 deletions.
20 changes: 15 additions & 5 deletions MainDemo.Wpf/SmartHint.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,11 @@
</Grid>

<!-- Reveal style PasswordBox variants -->
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="PasswordBox 'reveal' styles" Margin="0,40,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,40,0,0">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="PasswordBox 'reveal' styles" />
<CheckBox x:Name="PasswordBoxesRevealedCheckBox" Content="IsPasswordRevealed" Margin="20,0,0,0" />
</StackPanel>

<Grid>
<Grid.Resources>
<Style TargetType="{x:Type PasswordBox}" BasedOn="{StaticResource MaterialDesignFloatingHintRevealPasswordBox}">
Expand All @@ -417,6 +421,7 @@
<Setter Property="materialDesign:TextFieldAssist.LeadingIcon" Value="{StaticResource LeadingIcon}" />
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
<Setter Property="materialDesign:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
<Setter Property="materialDesign:PasswordBoxAssist.IsPasswordRevealed" Value="{Binding ElementName=PasswordBoxesRevealedCheckBox, Path=IsChecked}" />
<Setter Property="Padding">
<Setter.Value>
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
Expand Down Expand Up @@ -458,6 +463,7 @@
<Setter Property="materialDesign:TextFieldAssist.LeadingIcon" Value="{StaticResource LeadingIcon}" />
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
<Setter Property="materialDesign:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
<Setter Property="materialDesign:PasswordBoxAssist.IsPasswordRevealed" Value="{Binding ElementName=PasswordBoxesRevealedCheckBox, Path=IsChecked}" />
<Setter Property="Padding">
<Setter.Value>
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
Expand Down Expand Up @@ -499,6 +505,7 @@
<Setter Property="materialDesign:TextFieldAssist.LeadingIcon" Value="{StaticResource LeadingIcon}" />
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
<Setter Property="materialDesign:PasswordBoxAssist.Password" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
<Setter Property="materialDesign:PasswordBoxAssist.IsPasswordRevealed" Value="{Binding ElementName=PasswordBoxesRevealedCheckBox, Path=IsChecked}" />
<Setter Property="Padding">
<Setter.Value>
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
Expand Down Expand Up @@ -534,7 +541,10 @@
</Grid>

<!-- ComboBox variants -->
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="ComboBox styles" Margin="0,40,0,0" />
<StackPanel Orientation="Horizontal" Margin="0,40,0,0">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="ComboBox styles" />
<CheckBox x:Name="ComboBoxesEditableCheckBox" Content="IsEditable" Margin="20,0,0,0" />
</StackPanel>
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type ComboBox}" BasedOn="{StaticResource MaterialDesignFloatingHintComboBox}">
Expand All @@ -544,7 +554,7 @@
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
<Setter Property="ItemsSource" Value="{Binding ComboBoxOptions}" />
<Setter Property="IsEditable" Value="True" />
<Setter Property="IsEditable" Value="{Binding ElementName=ComboBoxesEditableCheckBox, Path=IsChecked}" />
<Setter Property="Padding">
<Setter.Value>
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
Expand Down Expand Up @@ -587,7 +597,7 @@
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
<Setter Property="ItemsSource" Value="{Binding ComboBoxOptions}" />
<Setter Property="IsEditable" Value="True" />
<Setter Property="IsEditable" Value="{Binding ElementName=ComboBoxesEditableCheckBox, Path=IsChecked}" />
<Setter Property="Padding">
<Setter.Value>
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
Expand Down Expand Up @@ -630,7 +640,7 @@
<Setter Property="materialDesign:HintAssist.IsFloating" Value="{Binding FloatHint}" />
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" />
<Setter Property="ItemsSource" Value="{Binding ComboBoxOptions}" />
<Setter Property="IsEditable" Value="True" />
<Setter Property="IsEditable" Value="{Binding ElementName=ComboBoxesEditableCheckBox, Path=IsChecked}" />
<Setter Property="Padding">
<Setter.Value>
<MultiBinding Converter="{StaticResource CustomPaddingConverter}">
Expand Down
37 changes: 37 additions & 0 deletions MaterialDesignThemes.UITests/WPF/PasswordBoxes/PasswordBoxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,41 @@ public async Task PasswordBox_WithHintAndValidationError_RespectsPadding(string

recorder.Success();
}

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

var stackPanel = await LoadXaml<StackPanel>(@"
<StackPanel Orientation=""Vertical"">
<TextBox x:Name=""TextBox1"" Width=""200"" />
<PasswordBox x:Name=""PasswordBox"" Width=""200""
materialDesign:PasswordBoxAssist.IsPasswordRevealed=""True""
Style=""{StaticResource MaterialDesignFloatingHintRevealPasswordBox}"" />
<TextBox x:Name=""TextBox2"" Width=""200"" />
</StackPanel>");

var textBox1 = await stackPanel.GetElement<TextBox>("TextBox1");
var passwordBox = await stackPanel.GetElement<PasswordBox>("PasswordBox");
var revealPasswordTextBox = await passwordBox.GetElement<TextBox>("RevealPasswordTextBox");
var textBox2 = await stackPanel.GetElement<TextBox>("TextBox2");

// Assert Tab forward
await textBox1.MoveKeyboardFocus();
Assert.True(await textBox1.GetIsKeyboardFocused());
await textBox1.SendKeyboardInput($"{Key.Tab}");
Assert.True(await revealPasswordTextBox.GetIsKeyboardFocused());
await revealPasswordTextBox.SendKeyboardInput($"{Key.Tab}");
Assert.True(await textBox2.GetIsKeyboardFocused());

// Assert Tab backwards
await textBox2.SendKeyboardInput($"{ModifierKeys.Shift}{Key.Tab}");
Assert.True(await revealPasswordTextBox.GetIsKeyboardFocused());
await revealPasswordTextBox.SendKeyboardInput($"{Key.Tab}{ModifierKeys.None}");
Assert.True(await textBox1.GetIsKeyboardFocused());

recorder.Success();
}
}
24 changes: 24 additions & 0 deletions MaterialDesignThemes.Wpf/Behaviors/PasswordBoxBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,41 @@ internal class PasswordBoxBehavior : Behavior<PasswordBox>
{
private void PasswordBoxLoaded(object sender, RoutedEventArgs e) => PasswordBoxAssist.SetPassword(AssociatedObject, AssociatedObject.Password);

private void PasswordBoxPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
if (PasswordBoxAssist.GetIsPasswordRevealed(AssociatedObject) &&
AssociatedObject.FindChild<TextBox>("RevealPasswordTextBox") is { } revealPasswordTextBox)
{
if (ReferenceEquals(e.OldFocus, revealPasswordTextBox) && ReferenceEquals(e.NewFocus, AssociatedObject))
{
// 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
TraversalRequest request = new TraversalRequest(FocusNavigationDirection.Previous);
AssociatedObject.MoveFocus(request);
e.Handled = true;
}
else if (!ReferenceEquals(e.OriginalSource, revealPasswordTextBox))
{
// When password box receives keyboard focus while the password is revealed, we transfer the focus to the nested reveal TextBox.
revealPasswordTextBox.Focus();
e.Handled = true;
}
}

}

protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += PasswordBoxLoaded;
AssociatedObject.PreviewGotKeyboardFocus += PasswordBoxPreviewGotKeyboardFocus;
}

protected override void OnDetaching()
{
if (AssociatedObject != null)
{
AssociatedObject.Loaded -= PasswordBoxLoaded;
AssociatedObject.PreviewGotKeyboardFocus -= PasswordBoxPreviewGotKeyboardFocus;
}
base.OnDetaching();
}
Expand Down

0 comments on commit 5881263

Please sign in to comment.