[Windows] Fix Narrator announcing ContentView children twice when Description is set#33979
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a Windows Narrator accessibility issue where ContentView content was announced twice when SemanticProperties.Description is set, by customizing the UI Automation peer used by the Windows ContentPanel.
Changes:
- Added a custom
AutomationPeerforContentPanelto alter control type/localized type and hide children whenAutomationProperties.Name(Description) is set. - Updated Windows PublicAPI unshipped list to include the new
OnCreateAutomationPeer()override. - Added Windows device tests validating automation peer behavior with/without Description.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/Core/src/Platform/Windows/ContentPanel.cs | Introduces a custom FrameworkElementAutomationPeer to prevent duplicate Narrator announcements when a Description/Name is present. |
| src/Core/src/PublicAPI/net-windows/PublicAPI.Unshipped.txt | Records the new protected override as an unshipped Windows API surface change. |
| src/Controls/tests/DeviceTests/Elements/ContentView/ContentViewTests.Windows.cs | Adds device tests validating control type, localized control type, and child visibility behavior under UIA. |
🤖 AI Summary📊 Expand Full Review —
|
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| PR | PR #33979 | Custom ContentPanelAutomationPeer nested class; hides children and uses AutomationControlType.Text when AutomationProperties.Name is set |
❌ FAILED (Gate — build env issue) | ContentPanel.cs, ContentViewTests.Windows.cs, PublicAPI.Unshipped.txt |
Gate failure is environment constraint, not code defect |
🔧 Fix — Analysis & Comparison
Fix Candidates
| # | Source | Approach | Test Result | Files Changed | Notes |
|---|---|---|---|---|---|
| 1 | try-fix (claude-opus-4.6) | Set AccessibilityView.Raw on ContentPanel children in UpdateSemantics (ViewExtensions.cs) when description is set |
ViewExtensions.cs, ContentViewTests.Windows.cs |
Build env: WindowsAppSDKSelfContained architecture error | |
| 2 | try-fix (claude-sonnet-4.6) | Cached _hasSemanticDescription flag via RegisterPropertyChangedCallback(NameProperty); peer reads field instead of calling GetName() |
ContentPanel.cs, PublicAPI.Unshipped.txt |
Build env: same architecture error | |
| 3 | try-fix (gpt-5.3-codex) | MauiPanelAutomationPeer override in MauiPanel base class instead of ContentPanel |
MauiPanel.cs, ContentViewTests.Windows.cs |
Build env: same architecture error | |
| 4 | try-fix (gpt-5.4) | Handler-level fix: ContentViewHandler.Windows.cs sets AccessibilityView.Raw on child element when parent has description |
ContentViewHandler.Windows.cs, ContentViewTests.Windows.cs |
Build env: same architecture error | |
| PR | PR #33979 | Custom ContentPanelAutomationPeer nested class; hides children and uses AutomationControlType.Text when name is set |
❌ FAILED (Gate — build env) | ContentPanel.cs, ContentViewTests.Windows.cs, PublicAPI.Unshipped.txt |
Gate failure is build env issue, not code defect |
Cross-Pollination
| Model | Round | New Ideas? | Details |
|---|---|---|---|
| claude-opus-4.6 | 2 | NO NEW IDEAS | All 5 approaches cover the two realistic solution categories (custom peer vs. AccessibilityView.Raw). No new variants remain. PR's peer-based approach is the standard WinUI pattern. |
Exhausted: Yes — all 4 models queried; cross-pollination complete; no new ideas.
Selected Fix: PR's fix — ContentPanelAutomationPeer approach
Reason: All try-fix alternatives were BLOCKED by the same environment issue. Code analysis confirms the PR's approach is architecturally sound and follows the standard WinUI pattern for this exact problem. No empirically superior alternative could be identified.
📋 Report — Final Recommendation
⚠️ Final Recommendation: REQUEST CHANGES
Phase Status
| Phase | Status | Notes |
|---|---|---|
| Pre-Flight | ✅ COMPLETE | Windows-only a11y fix; 2 implementation files, 1 test file, 1 public API entry |
| Gate | ❌ FAILED | Windows — both "without fix" and "with fix" failed with WindowsAppSDKSelfContained requires a supported Windows architecture (build env) |
| Try-Fix | ✅ COMPLETE | 4 attempts, 0 passing (all BLOCKED by same build env issue); cross-pollination exhausted |
| Report | ✅ COMPLETE |
Summary
PR #33979 addresses a real Windows accessibility bug (#33373) where ContentView with SemanticProperties.Description causes Narrator to announce content twice. The fix is architecturally sound and the prior inline review comments have been addressed. However, the Gate ❌ FAILED — both "without fix" and "with fix" builds failed with WindowsAppSDKSelfContained requires a supported Windows architecture in Graphics.csproj. All 4 try-fix attempts encountered the same environment blocker, and cross-pollination found no new ideas.
The Gate failure is a build environment constraint, not a code logic defect. The CI agent running this review lacks a supported Windows architecture for WinAppSDK self-contained deployment. CI pipelines (maui-pr-devicetests) were triggered by a team member on 2026-04-09 and should validate the fix on a proper Windows build agent.
Root Cause (of the Bug)
WinUI TextBlock (used by Label) automatically exposes its Text to UIA. ContentPanel's default FrameworkElementAutomationPeer exposes both AutomationProperties.Name (set via SemanticProperties.Description) and all child elements. Windows has no single built-in API like Android's NoHideDescendants to suppress children while keeping the parent accessible, so both Tab navigation and Browse mode announce duplicate content.
Fix Quality
Implementation (ContentPanel.cs): ✅ Sound
GetChildrenCore()→nullwhen description is set (prevents duplicate UIA tree traversal)GetAutomationControlTypeCore()→Text(enables Browse mode navigation without "group"/"custom" suffixes)GetLocalizedControlTypeCore()→""(suppresses "text" announcement suffix)HasDescriptioncleanly centralizes the non-empty name check- Follows the existing
MauiButtonAutomationPeerpattern in MAUI - Prior inline review comments (indentation, comment accuracy) are resolved
Tests (ContentViewTests.Windows.cs): ✅ Correct
GetOrCreateAutomationPeeruses directnew ContentPanel.ContentPanelAutomationPeer(contentPanel)instantiation, correctly bypassing WinUI3CreatePeerForElement()limitationAssert.Null(children)is correct for WinUI3 (nullGetChildrenCore()→ nullGetChildren())Assert.NotEmpty(children)for the no-description case validates children remain visible[Category(TestCategory.ContentView)]inherited from class-level attribute inContentViewTests.cs- Prior inline review comment ("assert NotEmpty") is resolved
Public API (PublicAPI.Unshipped.txt): ✅ Correct
override Microsoft.Maui.Platform.ContentPanel.OnCreateAutomationPeer()properly declared
Minor issue (cosmetic):
ContentPanelAutomationPeeris declared asinternal partial class— thepartialmodifier is unnecessary since the class is not split across files. Should beinternal class ContentPanelAutomationPeer.
Selected Fix: PR's fix
All try-fix alternatives were BLOCKED by the same environment issue. Code analysis confirms the PR's ContentPanelAutomationPeer approach is the best available solution for the WinUI platform constraints.
Required Action
The PR author should confirm the tests pass on a proper Windows x64 CI environment (maui-pr-devicetests was triggered 2026-04-09). If CI passes, the only remaining change is cosmetic:
- Remove
partialfromContentPanelAutomationPeerdeclaration:- internal partial class ContentPanelAutomationPeer : FrameworkElementAutomationPeer + internal class ContentPanelAutomationPeer : FrameworkElementAutomationPeer
|
🚀 Dogfood this PR with:
curl -fsSL https://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 33979Or
iex "& { $(irm https://github.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 33979" |
The test has now been updated and verified. It is working as expected. |
Backport of dotnet#34801 to main. The official pack pipeline doesn't need iOS simulators — it only builds and packs NuGet packages. The simulator install step is timing out on macOS agents, blocking BAR build production. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
/azp run maui-pr-uitests , maui-pr-devicetests |
|
Azure Pipelines successfully started running 2 pipeline(s). |
Code Review — PR #33979Independent AssessmentWhat this changes: Adds a custom Inferred motivation: When a Reconciliation with PR NarrativeAgreement: My independent assessment matches the PR description. The root cause analysis is correct — WinUI has no equivalent to Android's Findings
|
…cription is set (#33979) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause WinUI `TextBlock` (used by `Label`) automatically exposes its `Text` to UI Automation. `ContentPanel` uses the default `FrameworkElementAutomationPeer`, which exposes both the parent’s `AutomationProperties.Name` and all child elements. Unlike Android (`NoHideDescendants`) or iOS (`AccessibilityElementsHidden`), Windows has no single property to hide descendants while keeping the parent accessible. As a result, both Tab navigation and Browse mode announced duplicate content. ### Description of Change Implemented a custom `ContentPanelAutomationPeer` that overrides three core UI Automation methods (GetAutomationControlTypeCore , `GetLocalizedControlTypeCore`, `GetChildrenCore`) to conditionally modify behavior when Description is present. When a Description exists, the control is exposed as `AutomationControlType.Text` (enables browse mode navigation; alternatives like Custom announce "custom" suffix, Group causes browse mode to skip the element), the "text" announcement suffix is suppressed via empty `GetLocalizedControlTypeCore()` return, and child elements are hidden by returning null from `GetChildrenCore()` to prevent duplication. When no Description is present, default behavior is preserved with `AutomationControlType.Custom` and children remain accessible. The `HasDescription` helper property centralizes the non-empty Description check across all three override methods, ensuring consistent conditional logic following the MAUI `AutomationPeer patterns. ### Issues Fixed Fixes #33373 ### Platforms Tested - [ ] iOS - [x] Android - [x] Windows - [x] Mac ### Screenshots | Before Fix | After Fix | |------------|-----------| | <video width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/c5f8f114-fbeb-42d1-8601-e75dad57a1a7" /> | <video width="350" alt="withfix" src="https://github.com/user-attachments/assets/d2405df5-64f3-4cd9-8b38-40911ce4fbd6" /> | ---------
…cription is set (dotnet#33979) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause WinUI `TextBlock` (used by `Label`) automatically exposes its `Text` to UI Automation. `ContentPanel` uses the default `FrameworkElementAutomationPeer`, which exposes both the parent’s `AutomationProperties.Name` and all child elements. Unlike Android (`NoHideDescendants`) or iOS (`AccessibilityElementsHidden`), Windows has no single property to hide descendants while keeping the parent accessible. As a result, both Tab navigation and Browse mode announced duplicate content. ### Description of Change Implemented a custom `ContentPanelAutomationPeer` that overrides three core UI Automation methods (GetAutomationControlTypeCore , `GetLocalizedControlTypeCore`, `GetChildrenCore`) to conditionally modify behavior when Description is present. When a Description exists, the control is exposed as `AutomationControlType.Text` (enables browse mode navigation; alternatives like Custom announce "custom" suffix, Group causes browse mode to skip the element), the "text" announcement suffix is suppressed via empty `GetLocalizedControlTypeCore()` return, and child elements are hidden by returning null from `GetChildrenCore()` to prevent duplication. When no Description is present, default behavior is preserved with `AutomationControlType.Custom` and children remain accessible. The `HasDescription` helper property centralizes the non-empty Description check across all three override methods, ensuring consistent conditional logic following the MAUI `AutomationPeer patterns. ### Issues Fixed Fixes dotnet#33373 ### Platforms Tested - [ ] iOS - [x] Android - [x] Windows - [x] Mac ### Screenshots | Before Fix | After Fix | |------------|-----------| | <video width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/c5f8f114-fbeb-42d1-8601-e75dad57a1a7" /> | <video width="350" alt="withfix" src="https://github.com/user-attachments/assets/d2405df5-64f3-4cd9-8b38-40911ce4fbd6" /> | ---------
…cription is set (#33979) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause WinUI `TextBlock` (used by `Label`) automatically exposes its `Text` to UI Automation. `ContentPanel` uses the default `FrameworkElementAutomationPeer`, which exposes both the parent’s `AutomationProperties.Name` and all child elements. Unlike Android (`NoHideDescendants`) or iOS (`AccessibilityElementsHidden`), Windows has no single property to hide descendants while keeping the parent accessible. As a result, both Tab navigation and Browse mode announced duplicate content. ### Description of Change Implemented a custom `ContentPanelAutomationPeer` that overrides three core UI Automation methods (GetAutomationControlTypeCore , `GetLocalizedControlTypeCore`, `GetChildrenCore`) to conditionally modify behavior when Description is present. When a Description exists, the control is exposed as `AutomationControlType.Text` (enables browse mode navigation; alternatives like Custom announce "custom" suffix, Group causes browse mode to skip the element), the "text" announcement suffix is suppressed via empty `GetLocalizedControlTypeCore()` return, and child elements are hidden by returning null from `GetChildrenCore()` to prevent duplication. When no Description is present, default behavior is preserved with `AutomationControlType.Custom` and children remain accessible. The `HasDescription` helper property centralizes the non-empty Description check across all three override methods, ensuring consistent conditional logic following the MAUI `AutomationPeer patterns. ### Issues Fixed Fixes #33373 ### Platforms Tested - [ ] iOS - [x] Android - [x] Windows - [x] Mac ### Screenshots | Before Fix | After Fix | |------------|-----------| | <video width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/c5f8f114-fbeb-42d1-8601-e75dad57a1a7" /> | <video width="350" alt="withfix" src="https://github.com/user-attachments/assets/d2405df5-64f3-4cd9-8b38-40911ce4fbd6" /> | ---------
…cription is set (#33979) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause WinUI `TextBlock` (used by `Label`) automatically exposes its `Text` to UI Automation. `ContentPanel` uses the default `FrameworkElementAutomationPeer`, which exposes both the parent’s `AutomationProperties.Name` and all child elements. Unlike Android (`NoHideDescendants`) or iOS (`AccessibilityElementsHidden`), Windows has no single property to hide descendants while keeping the parent accessible. As a result, both Tab navigation and Browse mode announced duplicate content. ### Description of Change Implemented a custom `ContentPanelAutomationPeer` that overrides three core UI Automation methods (GetAutomationControlTypeCore , `GetLocalizedControlTypeCore`, `GetChildrenCore`) to conditionally modify behavior when Description is present. When a Description exists, the control is exposed as `AutomationControlType.Text` (enables browse mode navigation; alternatives like Custom announce "custom" suffix, Group causes browse mode to skip the element), the "text" announcement suffix is suppressed via empty `GetLocalizedControlTypeCore()` return, and child elements are hidden by returning null from `GetChildrenCore()` to prevent duplication. When no Description is present, default behavior is preserved with `AutomationControlType.Custom` and children remain accessible. The `HasDescription` helper property centralizes the non-empty Description check across all three override methods, ensuring consistent conditional logic following the MAUI `AutomationPeer patterns. ### Issues Fixed Fixes #33373 ### Platforms Tested - [ ] iOS - [x] Android - [x] Windows - [x] Mac ### Screenshots | Before Fix | After Fix | |------------|-----------| | <video width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/c5f8f114-fbeb-42d1-8601-e75dad57a1a7" /> | <video width="350" alt="withfix" src="https://github.com/user-attachments/assets/d2405df5-64f3-4cd9-8b38-40911ce4fbd6" /> | ---------
…cription is set (#33979) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Root Cause WinUI `TextBlock` (used by `Label`) automatically exposes its `Text` to UI Automation. `ContentPanel` uses the default `FrameworkElementAutomationPeer`, which exposes both the parent’s `AutomationProperties.Name` and all child elements. Unlike Android (`NoHideDescendants`) or iOS (`AccessibilityElementsHidden`), Windows has no single property to hide descendants while keeping the parent accessible. As a result, both Tab navigation and Browse mode announced duplicate content. ### Description of Change Implemented a custom `ContentPanelAutomationPeer` that overrides three core UI Automation methods (GetAutomationControlTypeCore , `GetLocalizedControlTypeCore`, `GetChildrenCore`) to conditionally modify behavior when Description is present. When a Description exists, the control is exposed as `AutomationControlType.Text` (enables browse mode navigation; alternatives like Custom announce "custom" suffix, Group causes browse mode to skip the element), the "text" announcement suffix is suppressed via empty `GetLocalizedControlTypeCore()` return, and child elements are hidden by returning null from `GetChildrenCore()` to prevent duplication. When no Description is present, default behavior is preserved with `AutomationControlType.Custom` and children remain accessible. The `HasDescription` helper property centralizes the non-empty Description check across all three override methods, ensuring consistent conditional logic following the MAUI `AutomationPeer patterns. ### Issues Fixed Fixes #33373 ### Platforms Tested - [ ] iOS - [x] Android - [x] Windows - [x] Mac ### Screenshots | Before Fix | After Fix | |------------|-----------| | <video width="350" alt="withoutfix" src="https://github.com/user-attachments/assets/c5f8f114-fbeb-42d1-8601-e75dad57a1a7" /> | <video width="350" alt="withfix" src="https://github.com/user-attachments/assets/d2405df5-64f3-4cd9-8b38-40911ce4fbd6" /> | ---------
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Root Cause
WinUI
TextBlock(used byLabel) automatically exposes itsTextto UI Automation.ContentPaneluses the defaultFrameworkElementAutomationPeer, which exposes both the parent’sAutomationProperties.Nameand all child elements. Unlike Android (NoHideDescendants) or iOS (AccessibilityElementsHidden), Windows has no single property to hide descendants while keeping the parent accessible. As a result, both Tab navigation and Browse mode announced duplicate content.Description of Change
Implemented a custom
ContentPanelAutomationPeerthat overrides three core UI Automation methods (GetAutomationControlTypeCore ,GetLocalizedControlTypeCore,GetChildrenCore) to conditionally modify behavior when Description is present.When a Description exists, the control is exposed as
AutomationControlType.Text(enables browse mode navigation; alternatives like Custom announce "custom" suffix, Group causes browse mode to skip the element), the "text" announcement suffix is suppressed via emptyGetLocalizedControlTypeCore()return, and child elements are hidden by returning null fromGetChildrenCore()to prevent duplication. When no Description is present, default behavior is preserved withAutomationControlType.Customand children remain accessible.The
HasDescriptionhelper property centralizes the non-empty Description check across all three override methods, ensuring consistent conditional logic following the MAUI `AutomationPeer patterns.Issues Fixed
Fixes #33373
Platforms Tested
Screenshots
WITHOUTFIX.1.mp4
WITHFIX.1.1.mp4