Skip to content

Commit afc3e1e

Browse files
authored
Fix screen reader button accessibility and category navigation (#4944)
* Fix UI automation peer wrapping and category focusability * Add accessibility name to SettingsButton * Avoid nesting ScrollViewer in generated panels with outer viewers Inspect targetGrid and its parent hierarchy in Invoke-WPFUIElements to avoid adding an inner ScrollViewer when an outer ScrollViewer already exists.
1 parent 53fc260 commit afc3e1e

4 files changed

Lines changed: 88 additions & 48 deletions

File tree

functions/private/Find-TweaksByNameOrDescription.ps1

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,25 @@ function Find-TweaksByNameOrDescription {
9090
}
9191

9292
if ($dockPanel -is [Windows.Controls.DockPanel]) {
93-
$itemsControl = $null
94-
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1
93+
$container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1
9594

96-
if ($null -ne $itemsControl) {
95+
if ($null -ne $container) {
96+
$targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container }
97+
$items = $null
98+
if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") {
99+
$items = $targetPanel.Items
100+
}
101+
else {
102+
$items = $targetPanel.Children
103+
}
97104
# Show all items in the category
98-
foreach ($item in $itemsControl.Items) {
105+
foreach ($item in $items) {
99106
if ($null -ne $item) {
100-
# Check if it's a category label (first Label in the ItemsControl)
101-
if ($item -is [Windows.Controls.Label]) {
107+
# Check if it's a category label (first Label in the container)
108+
if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") {
102109
$item.Visibility = [Windows.Visibility]::Visible
103110
}
104-
elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel]) {
111+
elseif ($item -is [Windows.Controls.DockPanel] -or $item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "DockPanel" -or $item.GetType().Name -eq "StackPanel") {
105112
# Show all checkbox containers
106113
$item.Visibility = [Windows.Visibility]::Visible
107114
}
@@ -143,16 +150,21 @@ function Find-TweaksByNameOrDescription {
143150
}
144151

145152
if ($dockPanel -is [Windows.Controls.DockPanel]) {
146-
$itemsControl = $null
147-
$itemsControl = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] } | Select-Object -First 1
153+
$container = $dockPanel.Children | Where-Object { $_ -is [Windows.Controls.ItemsControl] -or $_ -is [Windows.Controls.StackPanel] -or $_ -is [Windows.Controls.ScrollViewer] -or $_.GetType().Name -eq "ItemsControl" } | Select-Object -First 1
148154

149-
if ($null -ne $itemsControl) {
155+
if ($null -ne $container) {
150156
$categoryLabel = $null
151157

152-
# Process all items (checkboxes, labels, panels) in the ItemsControl
153-
for ($i = 0; $i -lt $itemsControl.Items.Count; $i++) {
154-
$item = $itemsControl.Items[$i]
155-
158+
$targetPanel = if ($container.PSObject.Properties['Content'] -and $null -ne $container.Content) { $container.Content } else { $container }
159+
$items = $null
160+
if ($targetPanel -is [Windows.Controls.ItemsControl] -or $targetPanel.GetType().Name -eq "ItemsControl") {
161+
$items = $targetPanel.Items
162+
}
163+
else {
164+
$items = $targetPanel.Children
165+
}
166+
# Process all items (checkboxes, labels, panels) in the container
167+
foreach ($item in $items) {
156168
if ($null -eq $item) {
157169
continue
158170
}
@@ -161,7 +173,7 @@ function Find-TweaksByNameOrDescription {
161173
# Check if this is a category label (usually first Label)
162174
# ------------------------------------------------------------
163175

164-
if ($item -is [Windows.Controls.Label]) {
176+
if ($item -is [Windows.Controls.Label] -or $item.GetType().Name -eq "Label") {
165177
$categoryLabel = $item
166178
# Initially hide category label; show it only if matches found
167179
$item.Visibility = [Windows.Visibility]::Collapsed
@@ -171,13 +183,13 @@ function Find-TweaksByNameOrDescription {
171183
# Check if this is a DockPanel containing a tweak checkbox
172184
# ------------------------------------------------------------
173185

174-
elseif ($item -is [Windows.Controls.DockPanel]) {
186+
elseif ($item -is [Windows.Controls.DockPanel] -or $item.GetType().Name -eq "DockPanel") {
175187
$checkbox = $null
176188
$label = $null
177189

178190
# Safely extract checkbox and label
179-
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1
180-
$label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] } | Select-Object -First 1
191+
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1
192+
$label = $item.Children | Where-Object { $_ -is [Windows.Controls.Label] -or $_.GetType().Name -eq "Label" } | Select-Object -First 1
181193

182194
# Check if tweak matches search criteria
183195
$itemMatches = $false
@@ -221,9 +233,9 @@ function Find-TweaksByNameOrDescription {
221233
# Check if this is a StackPanel containing a tweak checkbox
222234
# ------------------------------------------------------------
223235

224-
elseif ($item -is [Windows.Controls.StackPanel]) {
236+
elseif ($item -is [Windows.Controls.StackPanel] -or $item.GetType().Name -eq "StackPanel") {
225237
$checkbox = $null
226-
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] } | Select-Object -First 1
238+
$checkbox = $item.Children | Where-Object { $_ -is [Windows.Controls.CheckBox] -or $_.GetType().Name -eq "CheckBox" } | Select-Object -First 1
227239

228240
$itemMatches = $false
229241

functions/public/Invoke-WPFUIElements.ps1

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function Invoke-WPFUIElements {
4949
# Add ColumnDefinitions to the target Grid
5050
for ($i = 0; $i -lt $columncount; $i++) {
5151
$colDef = New-Object Windows.Controls.ColumnDefinition
52-
$colDef.Width = New-Object Windows.GridLength(1, [Windows.GridUnitType]::Star)
52+
$colDef.Width = New-Object System.Windows.GridLength([double]1, [System.Windows.GridUnitType]::Star)
5353
$targetGrid.ColumnDefinitions.Add($colDef) | Out-Null
5454
}
5555

@@ -113,36 +113,55 @@ function Invoke-WPFUIElements {
113113
$dockPanelContainer = New-Object Windows.Controls.DockPanel
114114
$border.Child = $dockPanelContainer
115115

116-
# Create an ItemsControl for application content
117-
$itemsControl = New-Object Windows.Controls.ItemsControl
118-
$itemsControl.HorizontalAlignment = 'Stretch'
119-
$itemsControl.VerticalAlignment = 'Stretch'
120-
121-
# Set the ItemsPanel to a VirtualizingStackPanel
122-
$itemsPanelTemplate = New-Object Windows.Controls.ItemsPanelTemplate
123-
$factory = New-Object Windows.FrameworkElementFactory ([Windows.Controls.VirtualizingStackPanel])
124-
$itemsPanelTemplate.VisualTree = $factory
125-
$itemsControl.ItemsPanel = $itemsPanelTemplate
126-
127-
# Set virtualization properties
128-
$itemsControl.SetValue([Windows.Controls.VirtualizingStackPanel]::IsVirtualizingProperty, $true)
129-
$itemsControl.SetValue([Windows.Controls.VirtualizingStackPanel]::VirtualizationModeProperty, [Windows.Controls.VirtualizationMode]::Recycling)
116+
# Create a StackPanel for application content controls
117+
$stackPanelContainer = New-Object Windows.Controls.StackPanel
118+
$stackPanelContainer.HorizontalAlignment = 'Stretch'
119+
$stackPanelContainer.VerticalAlignment = 'Stretch'
120+
121+
# Check if the target grid (or any ancestor) is already inside a ScrollViewer
122+
$hasOuterScrollViewer = $false
123+
$currentElement = $targetGrid
124+
while ($null -ne $currentElement) {
125+
if ($currentElement -is [System.Windows.Controls.ScrollViewer] -or $currentElement.GetType().Name -eq "ScrollViewer") {
126+
$hasOuterScrollViewer = $true
127+
break
128+
}
129+
$currentElement = $currentElement.Parent
130+
}
130131

131-
# Add the ItemsControl directly to the DockPanel
132-
[Windows.Controls.DockPanel]::SetDock($itemsControl, [Windows.Controls.Dock]::Bottom)
133-
$dockPanelContainer.Children.Add($itemsControl) | Out-Null
132+
if ($hasOuterScrollViewer) {
133+
# Add StackPanel directly to DockPanel without nesting a ScrollViewer
134+
[Windows.Controls.DockPanel]::SetDock($stackPanelContainer, [Windows.Controls.Dock]::Bottom)
135+
$dockPanelContainer.Children.Add($stackPanelContainer) | Out-Null
136+
}
137+
else {
138+
# Create a ScrollViewer for targets that do not already have an outer ScrollViewer
139+
$scrollViewer = New-Object Windows.Controls.ScrollViewer
140+
$scrollViewer.VerticalScrollBarVisibility = "Auto"
141+
$scrollViewer.HorizontalScrollBarVisibility = "Disabled"
142+
$scrollViewer.HorizontalAlignment = 'Stretch'
143+
$scrollViewer.VerticalAlignment = 'Stretch'
144+
$scrollViewer.Content = $stackPanelContainer
145+
146+
[Windows.Controls.DockPanel]::SetDock($scrollViewer, [Windows.Controls.Dock]::Bottom)
147+
$dockPanelContainer.Children.Add($scrollViewer) | Out-Null
148+
}
134149
$panelcount++
135150

136-
# Now proceed with adding category labels and entries to $itemsControl
151+
# Now proceed with adding category labels and entries to $stackPanelContainer
137152
foreach ($category in ($organizedData[$panelKey].Keys | Sort-Object)) {
138153
$count++
139154

140155
$label = New-Object Windows.Controls.Label
141-
$label.Content = $category -replace ".*__", ""
156+
$categoryCleanName = $category -replace ".*__", ""
157+
$label.Content = $categoryCleanName
158+
$label.Focusable = $true
159+
$label.IsTabStop = $true
160+
[System.Windows.Automation.AutomationProperties]::SetName($label, $categoryCleanName)
142161
$label.SetResourceReference([Windows.Controls.Control]::FontSizeProperty, "HeaderFontSize")
143162
$label.SetResourceReference([Windows.Controls.Control]::FontFamilyProperty, "HeaderFontFamily")
144163
$label.UseLayoutRounding = $true
145-
$itemsControl.Items.Add($label) | Out-Null
164+
$stackPanelContainer.Children.Add($label) | Out-Null
146165
$sync[$category] = $label
147166

148167
# Sort entries by type (checkboxes first, then buttons, then comboboxes, notes last) and then alphabetically by Content
@@ -177,7 +196,7 @@ function Invoke-WPFUIElements {
177196
$label.SetResourceReference([Windows.Controls.Control]::ForegroundProperty, "MainForegroundColor")
178197
$label.UseLayoutRounding = $true
179198
$dockPanel.Children.Add($label) | Out-Null
180-
$itemsControl.Items.Add($dockPanel) | Out-Null
199+
$stackPanelContainer.Children.Add($dockPanel) | Out-Null
181200

182201
$sync[$entryInfo.Name] = $checkBox
183202
$sync[$entryInfo.Name].IsChecked = (Get-WinUtilToggleStatus $entryInfo.Name)
@@ -215,7 +234,7 @@ function Invoke-WPFUIElements {
215234
contentOff = if ($entryInfo.Content.Count -ge 2) { $entryInfo.Content[1] } else { $contentOn }
216235
}
217236

218-
$itemsControl.Items.Add($toggleButton) | Out-Null
237+
$stackPanelContainer.Children.Add($toggleButton) | Out-Null
219238

220239
$sync[$entryInfo.Name] = $toggleButton
221240

@@ -295,7 +314,7 @@ function Invoke-WPFUIElements {
295314
}
296315

297316
$horizontalStackPanel.Children.Add($comboBox) | Out-Null
298-
$itemsControl.Items.Add($horizontalStackPanel) | Out-Null
317+
$stackPanelContainer.Children.Add($horizontalStackPanel) | Out-Null
299318

300319
if ($entryInfo.Registry -and @($entryInfo.Registry)[0].Values) {
301320
try {
@@ -387,7 +406,7 @@ function Invoke-WPFUIElements {
387406
$button.Width = [math]::Max($baseWidth, 350)
388407
}
389408
[System.Windows.Automation.AutomationProperties]::SetName($button, $entryInfo.Content)
390-
$itemsControl.Items.Add($button) | Out-Null
409+
$stackPanelContainer.Children.Add($button) | Out-Null
391410

392411
$sync[$entryInfo.Name] = $button
393412

@@ -414,7 +433,7 @@ function Invoke-WPFUIElements {
414433
$radioButtonGroups[$entryInfo.GroupName] = $groupStackPanel
415434

416435
# Add the group container to the ItemsControl
417-
$itemsControl.Items.Add($groupStackPanel) | Out-Null
436+
$stackPanelContainer.Children.Add($groupStackPanel) | Out-Null
418437
}
419438
else {
420439
# Retrieve the existing group container
@@ -459,7 +478,7 @@ function Invoke-WPFUIElements {
459478
$textBlock.Inlines.Add($bulletBadge)
460479
$textBlock.Inlines.Add($textRun)
461480

462-
$itemsControl.Items.Add($textBlock) | Out-Null
481+
$stackPanelContainer.Children.Add($textBlock) | Out-Null
463482
}
464483

465484
default {
@@ -519,7 +538,7 @@ function Invoke-WPFUIElements {
519538
$sync[$textBlock.Name] = $textBlock
520539
}
521540

522-
$itemsControl.Items.Add($horizontalStackPanel) | Out-Null
541+
$stackPanelContainer.Children.Add($horizontalStackPanel) | Out-Null
523542
$sync[$entryInfo.Name] = $checkBox
524543

525544
$sync[$entryInfo.Name].Add_Checked({

pester/lazy-tabs.Tests.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,11 @@ Describe "Startup lazy tab wiring" {
138138
$rendererScript | Should -Match '(?s)if \(\$entryInfo\.Link\).*\$textBlock\.Add_MouseUp\(\{.*Start-Process \$Sender\.ToolTip -ErrorAction Stop'
139139
$mainScript | Should -Not -Match '\.Name\.EndsWith\("Link"\)'
140140
}
141+
142+
It "checks for an existing outer ScrollViewer before nesting an inner ScrollViewer" {
143+
$rendererScript = Get-Content -Path (Join-Path $script:repoRoot "functions\public\Invoke-WPFUIElements.ps1") -Raw
144+
145+
$rendererScript | Should -Match '\$hasOuterScrollViewer'
146+
$rendererScript | Should -Match 'if\s*\(\$hasOuterScrollViewer\)'
147+
}
141148
}

xaml/inputXML.xaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,8 @@
12571257
HorizontalAlignment="Right" VerticalAlignment="Center"
12581258
Margin="0,0,2,0"
12591259
FontFamily="Segoe MDL2 Assets"
1260+
ToolTip="Settings"
1261+
AutomationProperties.Name="Settings"
12601262
Content=""/>
12611263
<Popup Name="SettingsPopup"
12621264
IsOpen="False"

0 commit comments

Comments
 (0)