@@ -9,11 +9,16 @@ import AppKit
99
1010/// # Notes
1111///
12- /// This implementation considers the entire document as one element, ignoring all subviews and lines.
12+ /// ~~ This implementation considers the entire document as one element, ignoring all subviews and lines.
1313/// Another idea would be to make each line fragment an accessibility element, with options for navigating through
1414/// lines from there. The text view would then only handle text input, and lines would handle reading out useful data
1515/// to the user.
16- /// More research needs to be done for the best option here.
16+ /// More research needs to be done for the best option here.~~
17+ ///
18+ /// Consider that the system has access to the ``TextView/accessibilityVisibleCharacterRange`` and
19+ /// ``TextView/accessibilityString(for:)`` methods. These can combine to allow an accessibility system to efficiently
20+ /// query the text view's contents. Adding accessibility elements to line fragments would require hit testing them,
21+ /// which will cause performance degradation.
1722extension TextView {
1823 override open func isAccessibilityElement( ) -> Bool {
1924 true
@@ -27,6 +32,11 @@ extension TextView {
2732 isFirstResponder
2833 }
2934
35+ override open func setAccessibilityFocused( _ accessibilityFocused: Bool ) {
36+ guard !isFirstResponder else { return }
37+ window? . makeFirstResponder ( self )
38+ }
39+
3040 override open func accessibilityLabel( ) -> String ? {
3141 " Text Editor "
3242 }
@@ -48,21 +58,26 @@ extension TextView {
4858 }
4959
5060 override open func accessibilityString( for range: NSRange ) -> String ? {
51- textStorage. substring (
61+ guard documentRange. intersection ( range) == range else {
62+ return nil
63+ }
64+
65+ return textStorage. substring (
5266 from: textStorage. mutableString. rangeOfComposedCharacterSequences ( for: range)
5367 )
5468 }
5569
5670 // MARK: Selections
5771
5872 override open func accessibilitySelectedText( ) -> String ? {
59- guard let selection = selectionManager
60- . textSelections
61- . sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } )
62- . first else {
73+ let selectedRange = accessibilitySelectedTextRange ( )
74+ guard selectedRange != . notFound else {
6375 return nil
6476 }
65- let range = ( textStorage. string as NSString ) . rangeOfComposedCharacterSequences ( for: selection. range)
77+ if selectedRange. isEmpty {
78+ return " "
79+ }
80+ let range = ( textStorage. string as NSString ) . rangeOfComposedCharacterSequences ( for: selectedRange)
6681 return textStorage. substring ( from: range)
6782 }
6883
@@ -71,7 +86,10 @@ extension TextView {
7186 . textSelections
7287 . sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } )
7388 . first else {
74- return . zero
89+ return . notFound
90+ }
91+ if selection. range. isEmpty {
92+ return selection. range
7593 }
7694 return textStorage. mutableString. rangeOfComposedCharacterSequences ( for: selection. range)
7795 }
@@ -83,12 +101,10 @@ extension TextView {
83101 }
84102
85103 override open func accessibilityInsertionPointLineNumber( ) -> Int {
86- guard let selection = selectionManager
87- . textSelections
88- . sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } )
89- . first,
90- let linePosition = layoutManager. textLineForOffset ( selection. range. location) else {
91- return 0
104+ let selectedRange = accessibilitySelectedTextRange ( )
105+ guard selectedRange != . notFound,
106+ let linePosition = layoutManager. textLineForOffset ( selectedRange. location) else {
107+ return - 1
92108 }
93109 return linePosition. index
94110 }
@@ -122,6 +138,31 @@ extension TextView {
122138 }
123139
124140 override open func accessibilityRange( for index: Int ) -> NSRange {
125- textStorage. mutableString. rangeOfComposedCharacterSequence ( at: index)
141+ guard index < documentRange. length else { return . notFound }
142+ return textStorage. mutableString. rangeOfComposedCharacterSequence ( at: index)
143+ }
144+
145+ override open func accessibilityVisibleCharacterRange( ) -> NSRange {
146+ visibleTextRange ?? . notFound
147+ }
148+
149+ /// The line index for a given character offset.
150+ override open func accessibilityLine( for index: Int ) -> Int {
151+ guard index <= textStorage. length,
152+ let textLine = layoutManager. textLineForOffset ( index) else {
153+ return - 1
154+ }
155+ return textLine. index
156+ }
157+
158+ override open func accessibilityFrame( for range: NSRange ) -> NSRect {
159+ guard documentRange. intersection ( range) == range else {
160+ return . zero
161+ }
162+ if range. isEmpty {
163+ return . zero
164+ }
165+ let rects = layoutManager. rectsFor ( range: range)
166+ return rects. boundingRect ( )
126167 }
127168}
0 commit comments