diff --git a/include/wx/osx/cocoa/private.h b/include/wx/osx/cocoa/private.h index e3658ff2c8..afcf152703 100644 --- a/include/wx/osx/cocoa/private.h +++ b/include/wx/osx/cocoa/private.h @@ -169,6 +169,7 @@ public : void SetFont(const wxFont & font) override; void SetToolTip( wxToolTip* tooltip ) override; void SetAccessibilityLabel(const wxString& label) override; + void SetAccessibilityTitleElement(wxWidgetImpl* title) override; void InstallEventHandler( WXWidget control = nullptr ) override; bool EnableTouchEvents(int eventsMask) override; diff --git a/include/wx/osx/core/private.h b/include/wx/osx/core/private.h index 4c6207a32c..632d0a8174 100644 --- a/include/wx/osx/core/private.h +++ b/include/wx/osx/core/private.h @@ -397,6 +397,7 @@ public : virtual void SetToolTip(wxToolTip* WXUNUSED(tooltip)) { } virtual void SetAccessibilityLabel(const wxString& WXUNUSED(label)) { } + virtual void SetAccessibilityTitleElement(wxWidgetImpl* WXUNUSED(title)) { } // is the clicked event sent AFTER the state already changed, so no additional // state changing logic is required from the outside diff --git a/include/wx/osx/stattext.h b/include/wx/osx/stattext.h index 654ccb5024..6f8e403cee 100644 --- a/include/wx/osx/stattext.h +++ b/include/wx/osx/stattext.h @@ -38,6 +38,8 @@ public: virtual bool AcceptsFocus() const override { return false; } + virtual wxOSXWidgetImpl* GetLabelPeer() const override { return GetPeer(); } + protected : virtual wxString WXGetVisibleLabel() const override; diff --git a/include/wx/osx/window.h b/include/wx/osx/window.h index f4ac0d3ae8..735f4db67e 100644 --- a/include/wx/osx/window.h +++ b/include/wx/osx/window.h @@ -280,6 +280,10 @@ public: // the 'true' OS level control for this wxWindow wxOSXWidgetImpl* GetPeer() const; + // the peer of this window if it is a label which can be used as the + // accessibility title of the next control, or nullptr otherwise + virtual wxOSXWidgetImpl* GetLabelPeer() const { return nullptr; } + // optimization to avoid creating a user pane in wxWindow::Create if we already know // we will replace it with our own peer void DontCreatePeer(); diff --git a/src/osx/cocoa/window.mm b/src/osx/cocoa/window.mm index 563a270ace..c732b7cab4 100644 --- a/src/osx/cocoa/window.mm +++ b/src/osx/cocoa/window.mm @@ -4042,6 +4042,24 @@ void wxWidgetCocoaImpl::SetAccessibilityLabel(const wxString& label) [view setAccessibilityLabel:str]; } +void wxWidgetCocoaImpl::SetAccessibilityTitleElement(wxWidgetImpl* title) +{ + // VoiceOver reads the view inside a scroll view, e.g. the text view of a + // multiline wxTextCtrl, and not the scroll view itself. + NSView* view = m_osxView; + if ( [view isKindOfClass:[NSScrollView class]] ) + { + NSView* const documentView = [(NSScrollView*)view documentView]; + if ( documentView ) + view = documentView; + } + + // For most controls the accessibility element is not the view itself but + // its cell, so link the elements actually used by VoiceOver. + [NSAccessibilityUnignoredDescendant(view) setAccessibilityTitleUIElement: + NSAccessibilityUnignoredDescendant(title->GetWXWidget())]; +} + void wxWidgetCocoaImpl::InstallEventHandler( WXWidget control ) { WXWidget c = control ? control : (WXWidget) m_osxView; diff --git a/src/osx/window_osx.cpp b/src/osx/window_osx.cpp index 3679bcff43..84ce04e43c 100644 --- a/src/osx/window_osx.cpp +++ b/src/osx/window_osx.cpp @@ -453,6 +453,17 @@ void wxWindowMac::MacPostControlCreate(const wxPoint& pos, } #endif + // Controls without their own label are typically preceded by a label + // describing them, which screen readers use as their name under MSW, so + // do the same here. + if ( GetLabel().empty() && !GetLabelPeer() ) + { + const wxWindow* const prev = GetPrevSibling(); + wxOSXWidgetImpl* const labelPeer = prev ? prev->GetLabelPeer() : nullptr; + if ( labelPeer ) + GetPeer()->SetAccessibilityTitleElement(labelPeer); + } + } void wxWindowMac::DoSetWindowVariant( wxWindowVariant variant )