Skip to content

Commit 255a214

Browse files
authored
refactor(ios): simplify accessibility text element creation (#188)
* refactor(ios): simplify accessibility text element creation * docs: enhance accessibility documentation with text announcements and known issues sections
1 parent d34f98f commit 255a214

2 files changed

Lines changed: 49 additions & 57 deletions

File tree

docs/ACCESSIBILITY.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
The library implements native accessibility features that enable screen readers (VoiceOver on iOS and TalkBack on Android) to properly navigate and understand Markdown content. This includes semantic labeling, custom navigation controls, and proper announcements for all supported elements.
88

9+
## Text Announcements
10+
11+
Plain text paragraphs without inline links or images are announced as a single VoiceOver element per paragraph. Paragraphs containing links or images are segmented into text, link, and image parts so that each remains independently navigable. List items follow the same logic — a list item without inline specials is a single element, while one containing a link is split accordingly. Whitespace-only segments between elements are filtered out to avoid empty announcements.
12+
913
## Supported Elements
1014

1115
| Element | VoiceOver (iOS) | TalkBack (Android) |
@@ -81,3 +85,7 @@ List items are announced with their position and type:
8185
**Nested Lists:**
8286
- iOS: Proper depth handling with semantic structure
8387
- Android: "Nested" prefix is added for items at deeper levels (e.g., "nested bullet point", "nested list item 1")
88+
89+
## Known Issues
90+
91+
- **Blockquote border with inline links (iOS):** When a blockquote contains inline links, the background/border may break at link boundaries instead of spanning the full line. This is a cosmetic limitation of `NSAttributedString` drawing separate background rects per attribute run and will be addressed in a future update.

ios/utils/MarkdownAccessibilityElementBuilder.m

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ @implementation MarkdownAccessibilityElementBuilder
3737
NSDictionary *list = [self listItemInfoForRange:paragraphRange info:info];
3838

3939
if (specials.count == 0) {
40-
[self addTextElementsPerLineTo:elements
41-
range:paragraphRange
42-
fullText:fullString
43-
heading:level
44-
listInfo:list
45-
view:textView
46-
container:container];
40+
[elements addObject:[self createElementForRange:paragraphRange
41+
type:ElementTypeText
42+
text:trimmed
43+
isLinked:NO
44+
heading:level
45+
listInfo:list
46+
view:textView
47+
container:container]];
4748
} else {
4849
[elements addObjectsFromArray:[self segmentedElementsForParagraph:paragraphRange
4950
fullText:fullString
@@ -61,6 +62,14 @@ @implementation MarkdownAccessibilityElementBuilder
6162

6263
#pragma mark - Segmentation
6364

65+
+ (BOOL)hasAlphanumericContent:(NSString *)text
66+
{
67+
static NSCharacterSet *alphanumericSet;
68+
static dispatch_once_t onceToken;
69+
dispatch_once(&onceToken, ^{ alphanumericSet = [NSCharacterSet alphanumericCharacterSet]; });
70+
return [text rangeOfCharacterFromSet:alphanumericSet].location != NSNotFound;
71+
}
72+
6473
+ (NSArray<UIAccessibilityElement *> *)segmentedElementsForParagraph:(NSRange)paragraphRange
6574
fullText:(NSString *)fullText
6675
headingLevel:(NSInteger)headingLevel
@@ -80,13 +89,18 @@ @implementation MarkdownAccessibilityElementBuilder
8089

8190
if (itemRange.location > segmentStart) {
8291
NSRange beforeRange = NSMakeRange(segmentStart, itemRange.location - segmentStart);
83-
[self addTextElementsPerLineTo:elements
84-
range:beforeRange
85-
fullText:fullText
86-
heading:headingLevel
87-
listInfo:listInfo
88-
view:textView
89-
container:container];
92+
NSString *beforeText = [[fullText substringWithRange:beforeRange]
93+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
94+
if (beforeText.length > 0 && [self hasAlphanumericContent:beforeText]) {
95+
[elements addObject:[self createElementForRange:beforeRange
96+
type:ElementTypeText
97+
text:beforeText
98+
isLinked:NO
99+
heading:headingLevel
100+
listInfo:listInfo
101+
view:textView
102+
container:container]];
103+
}
90104
}
91105

92106
BOOL isImg = item[@"altText"] != nil;
@@ -104,18 +118,23 @@ @implementation MarkdownAccessibilityElementBuilder
104118

105119
if (segmentStart < NSMaxRange(paragraphRange)) {
106120
NSRange afterRange = NSMakeRange(segmentStart, NSMaxRange(paragraphRange) - segmentStart);
107-
[self addTextElementsPerLineTo:elements
108-
range:afterRange
109-
fullText:fullText
110-
heading:headingLevel
111-
listInfo:listInfo
112-
view:textView
113-
container:container];
121+
NSString *afterText = [[fullText substringWithRange:afterRange]
122+
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
123+
if (afterText.length > 0 && [self hasAlphanumericContent:afterText]) {
124+
[elements addObject:[self createElementForRange:afterRange
125+
type:ElementTypeText
126+
text:afterText
127+
isLinked:NO
128+
heading:headingLevel
129+
listInfo:listInfo
130+
view:textView
131+
container:container]];
132+
}
114133
}
115134
return elements;
116135
}
117136

118-
#pragma mark - Factory & Precise Splitting
137+
#pragma mark - Factory
119138

120139
+ (UIAccessibilityElement *)createElementForRange:(NSRange)range
121140
type:(ElementType)type
@@ -159,41 +178,6 @@ + (UIAccessibilityElement *)createElementForRange:(NSRange)range
159178
return el;
160179
}
161180

162-
+ (void)addTextElementsPerLineTo:(NSMutableArray *)elements
163-
range:(NSRange)range
164-
fullText:(NSString *)fullText
165-
heading:(NSInteger)level
166-
listInfo:(NSDictionary *)listInfo
167-
view:(UITextView *)tv
168-
container:(id)c
169-
{
170-
NSLayoutManager *lm = tv.layoutManager;
171-
NSRange glyphRange = [lm glyphRangeForCharacterRange:range actualCharacterRange:NULL];
172-
173-
[lm enumerateLineFragmentsForGlyphRange:glyphRange
174-
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *tc, NSRange lineGlyphRange,
175-
BOOL *stop) {
176-
NSRange intersection = NSIntersectionRange(glyphRange, lineGlyphRange);
177-
if (intersection.length > 0) {
178-
NSRange charRange = [lm characterRangeForGlyphRange:intersection
179-
actualGlyphRange:NULL];
180-
NSString *trimmed = [[fullText substringWithRange:charRange]
181-
stringByTrimmingCharactersInSet:[NSCharacterSet
182-
whitespaceAndNewlineCharacterSet]];
183-
if (trimmed.length > 0) {
184-
[elements addObject:[self createElementForRange:charRange
185-
type:ElementTypeText
186-
text:trimmed
187-
isLinked:NO
188-
heading:level
189-
listInfo:listInfo
190-
view:tv
191-
container:c]];
192-
}
193-
}
194-
}];
195-
}
196-
197181
#pragma mark - Helpers
198182

199183
+ (NSString *)formatListAnnouncement:(NSDictionary *)info

0 commit comments

Comments
 (0)