From 59e7353a4596da1ccf405c637a729d4eaee41d7f Mon Sep 17 00:00:00 2001 From: Jumar Macato <16554748+jmacato@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:29:18 +0800 Subject: [PATCH] macOS: fix accessibility read-only, selection, and expand/collapse actions (#21599) * Gate macOS setAccessibilityValue on value read-only state A read-only value provider must not advertise the setAccessibilityValue selector, so AX clients see the value as not settable. * Gate macOS read-only range value mutations Report a read-only IRangeValueProvider (ProgressBar, read-only NumericUpDown) as not settable, and stop advertising setAccessibilityValue/increment/decrement for it; keep min/max exposed. * Support deselect and toggle expand/collapse in macOS AX setAccessibilitySelected:NO now removes from selection, and AXPress toggles an expand/collapse provider instead of always expanding. * Harden macOS AX select, value-set, and disabled-control actions setAccessibilitySelected:YES now adds to the selection instead of replacing it, the value setter respects read-only, and mutating actions are gated on IsEnabled. --- native/Avalonia.Native/src/OSX/automation.mm | 41 +++++++++++++------- src/Avalonia.Native/AvnAutomationPeer.cs | 3 ++ src/Avalonia.Native/avn.idl | 5 ++- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/automation.mm b/native/Avalonia.Native/src/OSX/automation.mm index f355634de4..8750e9986c 100644 --- a/native/Avalonia.Native/src/OSX/automation.mm +++ b/native/Avalonia.Native/src/OSX/automation.mm @@ -260,14 +260,16 @@ - (void)setAccessibilityValue:(id)newValue { - if (_peer->IsValueProvider()) + if (!_peer->IsEnabled()) + return; + if (_peer->IsValueProvider() && !_peer->ValueProvider_IsReadOnly()) { if (newValue == nil) _peer->ValueProvider_SetValue(nil); else if ([newValue isKindOfClass:[NSString class]]) _peer->ValueProvider_SetValue([(NSString*)newValue UTF8String]); } - else if (_peer->IsRangeValueProvider()) + else if (_peer->IsRangeValueProvider() && !_peer->RangeValueProvider_IsReadOnly()) { if ([newValue isKindOfClass:[NSNumber class]]) _peer->RangeValueProvider_SetValue([(NSNumber*)newValue doubleValue]); @@ -281,7 +283,7 @@ if (_peer->IsValueProvider()) return !_peer->ValueProvider_IsReadOnly(); if (_peer->IsRangeValueProvider()) - return YES; + return !_peer->RangeValueProvider_IsReadOnly(); return NO; } @@ -395,7 +397,7 @@ - (void)setAccessibilityExpanded:(BOOL)accessibilityExpanded { - if (!_peer->IsExpandCollapseProvider()) + if (!_peer->IsExpandCollapseProvider() || !_peer->IsEnabled()) return; if (accessibilityExpanded) _peer->ExpandCollapseProvider_Expand(); @@ -405,13 +407,18 @@ - (BOOL)accessibilityPerformPress { + if (!_peer->IsEnabled()) + return NO; if (_peer->IsInvokeProvider()) { _peer->InvokeProvider_Invoke(); } else if (_peer->IsExpandCollapseProvider()) { - _peer->ExpandCollapseProvider_Expand(); + if (_peer->ExpandCollapseProvider_GetIsExpanded()) + _peer->ExpandCollapseProvider_Collapse(); + else + _peer->ExpandCollapseProvider_Expand(); } else if (_peer->IsToggleProvider()) { @@ -422,7 +429,7 @@ - (BOOL)accessibilityPerformIncrement { - if (!_peer->IsRangeValueProvider()) + if (!_peer->IsRangeValueProvider() || _peer->RangeValueProvider_IsReadOnly() || !_peer->IsEnabled()) return NO; auto value = _peer->RangeValueProvider_GetValue(); value += _peer->RangeValueProvider_GetSmallChange(); @@ -432,7 +439,7 @@ - (BOOL)accessibilityPerformDecrement { - if (!_peer->IsRangeValueProvider()) + if (!_peer->IsRangeValueProvider() || _peer->RangeValueProvider_IsReadOnly() || !_peer->IsEnabled()) return NO; auto value = _peer->RangeValueProvider_GetValue(); value -= _peer->RangeValueProvider_GetSmallChange(); @@ -442,7 +449,7 @@ - (BOOL)accessibilityPerformShowMenu { - if (!_peer->IsExpandCollapseProvider()) + if (!_peer->IsExpandCollapseProvider() || !_peer->IsEnabled()) return NO; _peer->ExpandCollapseProvider_Expand(); return YES; @@ -476,15 +483,20 @@ - (void)setAccessibilitySelected:(BOOL)accessibilitySelected { - if (accessibilitySelected && _peer->IsSelectionItemProvider()) - _peer->SelectionItemProvider_Select(); + if (!_peer->IsSelectionItemProvider() || !_peer->IsEnabled()) + return; + if (accessibilitySelected) + _peer->SelectionItemProvider_AddToSelection(); + else + _peer->SelectionItemProvider_RemoveFromSelection(); } - (BOOL)isAccessibilitySelectorAllowed:(SEL)selector { if (selector == @selector(setAccessibilityValue:)) { - return _peer->IsValueProvider() || _peer->IsRangeValueProvider(); + return (_peer->IsValueProvider() && !_peer->ValueProvider_IsReadOnly()) || + (_peer->IsRangeValueProvider() && !_peer->RangeValueProvider_IsReadOnly()); } else if (selector == @selector(accessibilityPerformShowMenu)) { @@ -503,8 +515,11 @@ return _peer->IsSelectionItemProvider(); } else if (selector == @selector(accessibilityPerformIncrement) || - selector == @selector(accessibilityPerformDecrement) || - selector == @selector(accessibilityMinValue) || + selector == @selector(accessibilityPerformDecrement)) + { + return _peer->IsRangeValueProvider() && !_peer->RangeValueProvider_IsReadOnly(); + } + else if (selector == @selector(accessibilityMinValue) || selector == @selector(accessibilityMaxValue)) { return _peer->IsRangeValueProvider(); diff --git a/src/Avalonia.Native/AvnAutomationPeer.cs b/src/Avalonia.Native/AvnAutomationPeer.cs index b448d5cb1a..9b9f890c06 100644 --- a/src/Avalonia.Native/AvnAutomationPeer.cs +++ b/src/Avalonia.Native/AvnAutomationPeer.cs @@ -184,10 +184,13 @@ namespace Avalonia.Native public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange; public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange; public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value); + public int RangeValueProvider_IsReadOnly() => RangeValueProvider.IsReadOnly.AsComBool(); public int IsSelectionItemProvider() => IsProvider(); public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool(); public void SelectionItemProvider_Select() => SelectionItemProvider.Select(); + public void SelectionItemProvider_AddToSelection() => SelectionItemProvider.AddToSelection(); + public void SelectionItemProvider_RemoveFromSelection() => SelectionItemProvider.RemoveFromSelection(); public IAvnAutomationPeer? ScrollProvider_GetHorizontalScrollBar() => _inner is ScrollViewerAutomationPeer scrollViewer ? Wrap(scrollViewer.GetHorizontalScrollBarPeer()) : null; diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 5b1b97fa37..f01430a42a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1315,10 +1315,13 @@ interface IAvnAutomationPeer : IUnknown double RangeValueProvider_GetSmallChange(); double RangeValueProvider_GetLargeChange(); void RangeValueProvider_SetValue(double value); - + bool RangeValueProvider_IsReadOnly(); + bool IsSelectionItemProvider(); bool SelectionItemProvider_IsSelected(); void SelectionItemProvider_Select(); + void SelectionItemProvider_AddToSelection(); + void SelectionItemProvider_RemoveFromSelection(); IAvnAutomationPeer* ScrollProvider_GetHorizontalScrollBar(); IAvnAutomationPeer* ScrollProvider_GetVerticalScrollBar();