Skip to content

Commit b5f4df9

Browse files
brianrobCopilot
andauthored
Improve Keyboard Focus Indicator Contrast (#2448)
* Improve keyboard focus indicator contrast Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Isolate focus visual WPF tests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 11232b9e-2a12-482f-88c4-6b9023450660 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 11232b9e-2a12-482f-88c4-6b9023450660
1 parent 97e8f45 commit b5f4df9

5 files changed

Lines changed: 183 additions & 4 deletions

File tree

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
using PerfView;
2+
using System;
3+
using System.Linq;
4+
using System.Windows;
5+
using System.Windows.Controls;
6+
using System.Windows.Documents;
7+
using System.Windows.Media;
8+
using System.Windows.Shapes;
9+
using REghZyFramework.Themes;
10+
using Xunit;
11+
12+
namespace PerfViewTests.Accessibility
13+
{
14+
public class FocusVisualTests
15+
{
16+
private const double MinimumContrastRatio = 3.0;
17+
18+
[WpfFact]
19+
public void AccessibleFocusVisualMeetsContrastRequirements()
20+
{
21+
AssertThemeFocusVisual(
22+
"LightTheme.xaml",
23+
Colors.White,
24+
GetThemeColor("LightTheme.xaml", "ContainerBackground"),
25+
GetThemeColor("LightTheme.xaml", "ControlDefaultBackground"));
26+
27+
AssertThemeFocusVisual(
28+
"DarkTheme.xaml",
29+
GetThemeColor("DarkTheme.xaml", "ContainerBackground"),
30+
GetThemeColor("DarkTheme.xaml", "ControlDefaultBackground"));
31+
}
32+
33+
[WpfFact]
34+
public void ReportedControlsUseAccessibleFocusVisual()
35+
{
36+
ResourceDictionary theme = LoadTheme("LightTheme.xaml");
37+
38+
MainWindow mainWindow = null;
39+
try
40+
{
41+
App.CommandLineArgs = new CommandLineArgs();
42+
App.CommandProcessor = new CommandProcessor();
43+
mainWindow = new MainWindow(true);
44+
mainWindow.Resources.MergedDictionaries.Add(theme);
45+
46+
Style expectedStyle = (Style)theme["AccessibleFocusVisual"];
47+
Assert.Same(expectedStyle, mainWindow.Body.FocusVisualStyle);
48+
49+
StatusBar statusBar = mainWindow.StatusBar;
50+
Assert.Same(expectedStyle, ((TextBox)statusBar.FindName("m_StatusMessage")).FocusVisualStyle);
51+
Assert.Same(expectedStyle, ((Button)statusBar.FindName("m_LogButton")).FocusVisualStyle);
52+
Assert.Same(expectedStyle, ((Button)statusBar.FindName("m_CancelButton")).FocusVisualStyle);
53+
54+
Hyperlink welcomeLink = mainWindow.Body.Document.Blocks
55+
.OfType<Paragraph>()
56+
.SelectMany(paragraph => paragraph.Inlines)
57+
.OfType<Hyperlink>()
58+
.First();
59+
Assert.Same(expectedStyle, welcomeLink.FocusVisualStyle);
60+
}
61+
finally
62+
{
63+
mainWindow?.Close();
64+
}
65+
}
66+
67+
#region private
68+
69+
private static void AssertThemeFocusVisual(string themeName, params Color[] adjacentColors)
70+
{
71+
ResourceDictionary theme = LoadTheme(themeName);
72+
SolidColorBrush focusBrush = (SolidColorBrush)theme["AccessibleFocusVisualBrush"];
73+
Style focusStyle = (Style)theme["AccessibleFocusVisual"];
74+
Setter templateSetter = focusStyle.Setters
75+
.OfType<Setter>()
76+
.Single(setter => setter.Property == Control.TemplateProperty);
77+
Rectangle focusRectangle = (Rectangle)((ControlTemplate)templateSetter.Value).LoadContent();
78+
79+
Assert.True(focusRectangle.StrokeThickness >= 2);
80+
Assert.Equal(focusBrush.Color, ((SolidColorBrush)focusRectangle.Stroke).Color);
81+
82+
foreach (Color adjacentColor in adjacentColors)
83+
{
84+
double contrastRatio = GetContrastRatio(focusBrush.Color, adjacentColor);
85+
Assert.True(
86+
contrastRatio >= MinimumContrastRatio,
87+
$"{themeName} focus color {focusBrush.Color} has a contrast ratio of only {contrastRatio:F3}:1 against {adjacentColor}.");
88+
}
89+
}
90+
91+
private static Color GetThemeColor(string themeName, string resourceKey)
92+
{
93+
return ((SolidColorBrush)LoadTheme(themeName)[resourceKey]).Color;
94+
}
95+
96+
private static ResourceDictionary LoadTheme(string themeName)
97+
{
98+
if (themeName == "LightTheme.xaml")
99+
{
100+
var theme = new LightTheme();
101+
theme.InitializeComponent();
102+
return theme;
103+
}
104+
105+
if (themeName == "DarkTheme.xaml")
106+
{
107+
var theme = new DarkTheme();
108+
theme.InitializeComponent();
109+
return theme;
110+
}
111+
112+
throw new ArgumentException($"Unknown theme '{themeName}'.", nameof(themeName));
113+
}
114+
115+
private static double GetContrastRatio(Color first, Color second)
116+
{
117+
double firstLuminance = GetRelativeLuminance(first);
118+
double secondLuminance = GetRelativeLuminance(second);
119+
return (Math.Max(firstLuminance, secondLuminance) + 0.05) /
120+
(Math.Min(firstLuminance, secondLuminance) + 0.05);
121+
}
122+
123+
private static double GetRelativeLuminance(Color color)
124+
{
125+
return (0.2126 * GetLinearChannel(color.R)) +
126+
(0.7152 * GetLinearChannel(color.G)) +
127+
(0.0722 * GetLinearChannel(color.B));
128+
}
129+
130+
private static double GetLinearChannel(byte channel)
131+
{
132+
double value = channel / 255.0;
133+
return value <= 0.04045
134+
? value / 12.92
135+
: Math.Pow((value + 0.055) / 1.055, 2.4);
136+
}
137+
138+
#endregion
139+
}
140+
}

src/PerfView/GuiUtilities/StatusBar/StatusBar.xaml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
<ColumnDefinition Width="auto"/>
1414
<ColumnDefinition Width="auto"/>
1515
</Grid.ColumnDefinitions>
16-
<TextBox Grid.Column="0" Name="m_StatusMessage" ContextMenu="{x:Null}" IsReadOnly="True" VerticalScrollBarVisibility="Auto" AutomationProperties.Name="Status Message">
16+
<TextBox Grid.Column="0" Name="m_StatusMessage" ContextMenu="{x:Null}" IsReadOnly="True"
17+
FocusVisualStyle="{DynamicResource AccessibleFocusVisual}"
18+
VerticalScrollBarVisibility="Auto" AutomationProperties.Name="Status Message">
1719
<TextBox.Triggers>
1820
<EventTrigger RoutedEvent="perfview:StatusBar.HighlightMessage">
1921
<EventTrigger.Actions>
@@ -38,7 +40,11 @@
3840
</TextBox.Triggers>
3941
</TextBox>
4042
<TextBlock Grid.Column="1" Name ="m_ProgressText" Width="85" Text="Ready" Margin="5,0,0,0" TextAlignment="Left" VerticalAlignment="Center"></TextBlock>
41-
<Button Grid.Column="2" Name ="m_LogButton" IsEnabled="true" Margin="5,2" ToolTip="Open the diagnostic log" Click="Log_Click">Log</Button>
42-
<Button Grid.Column="3" Name ="m_CancelButton" IsEnabled="false" Margin="5,2" ToolTip="Cancel the operation in flight (ESC)" Click="Cancel_Click">Cancel</Button>
43+
<Button Grid.Column="2" Name ="m_LogButton" IsEnabled="true" Margin="5,2"
44+
FocusVisualStyle="{DynamicResource AccessibleFocusVisual}"
45+
ToolTip="Open the diagnostic log" Click="Log_Click">Log</Button>
46+
<Button Grid.Column="3" Name ="m_CancelButton" IsEnabled="false" Margin="5,2"
47+
FocusVisualStyle="{DynamicResource AccessibleFocusVisual}"
48+
ToolTip="Cancel the operation in flight (ESC)" Click="Cancel_Click">Cancel</Button>
4349
</Grid>
4450
</UserControl>

src/PerfView/MainWindow.xaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,15 @@
227227
<GridSplitter Grid.Column="1" Width="3" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
228228
<RichTextBox Name="Body" Grid.Column="2" IsReadOnly="True" IsDocumentEnabled="True"
229229
IsEnabled="{Binding ElementName=StatusBar, Path=IsNotWorking}"
230+
FocusVisualStyle="{DynamicResource AccessibleFocusVisual}"
230231
VerticalScrollBarVisibility="auto" AutomationProperties.Name="Usage Information">
231232
<RichTextBox.Resources>
232233
<Style TargetType="{x:Type Paragraph}">
233234
<Setter Property="Margin" Value="0"/>
234235
</Style>
235-
236+
<Style TargetType="{x:Type Hyperlink}" BasedOn="{StaticResource {x:Type Hyperlink}}">
237+
<Setter Property="FocusVisualStyle" Value="{DynamicResource AccessibleFocusVisual}"/>
238+
</Style>
236239
</RichTextBox.Resources>
237240
<RichTextBox.Document>
238241
<FlowDocument>

src/PerfView/Themes/DarkTheme.xaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ SOFTWARE.
6262

6363
<SolidColorBrush x:Key="AlternateRowBackground" Color="#FF424124" />
6464
<SolidColorBrush x:Key="HelpRibbonBackground" Color="#FF323838" />
65+
<SolidColorBrush x:Key="AccessibleFocusVisualBrush" Color="#FF60CDFF" />
6566

6667
<SolidColorBrush x:Key="ScrollBarBackground" Color="#2e2e2e" />
6768
<SolidColorBrush x:Key="ScrollBarControlForground" Color="#4d4d4d" />
@@ -111,6 +112,20 @@ SOFTWARE.
111112
</Setter>
112113
</Style>
113114

115+
<!-- Two-pixel keyboard focus visual for surfaces that require enhanced visibility. -->
116+
<Style x:Key="AccessibleFocusVisual">
117+
<Setter Property="Control.Template">
118+
<Setter.Value>
119+
<ControlTemplate>
120+
<Rectangle Margin="1"
121+
SnapsToDevicePixels="true"
122+
Stroke="{StaticResource AccessibleFocusVisualBrush}"
123+
StrokeThickness="2" />
124+
</ControlTemplate>
125+
</Setter.Value>
126+
</Setter>
127+
</Style>
128+
114129
<!-- -_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_-_- -->
115130

116131
<!-- RepeatButton -->

src/PerfView/Themes/LightTheme.xaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ SOFTWARE.
6363

6464
<SolidColorBrush x:Key="AlternateRowBackground" Color="#FFF2F1A4" />
6565
<SolidColorBrush x:Key="HelpRibbonBackground" Color="#FFE2E8F8" />
66+
<SolidColorBrush x:Key="AccessibleFocusVisualBrush" Color="#FF005FB8" />
6667

6768
<!-- Colourful theme Colours -->
6869

@@ -71,6 +72,20 @@ SOFTWARE.
7172
<SolidColorBrush x:Key="ControlBrightPrimaryColourBackground" Color="#FF3296FA" />
7273
<SolidColorBrush x:Key="ControlBrightPrimaryColourBorderBrush" Color="#FF50A4FA" />
7374

75+
<!-- Two-pixel keyboard focus visual for surfaces that require enhanced visibility. -->
76+
<Style x:Key="AccessibleFocusVisual">
77+
<Setter Property="Control.Template">
78+
<Setter.Value>
79+
<ControlTemplate>
80+
<Rectangle Margin="1"
81+
SnapsToDevicePixels="true"
82+
Stroke="{StaticResource AccessibleFocusVisualBrush}"
83+
StrokeThickness="2" />
84+
</ControlTemplate>
85+
</Setter.Value>
86+
</Setter>
87+
</Style>
88+
7489
<!-- Custom Window style (unaffected by win32 style stuff) -->
7590
<Style x:Key="CustomWindowStyle" TargetType="{x:Type Window}">
7691
<!-- Better for rendering small text -->

0 commit comments

Comments
 (0)