#569 fixed talkBack skipping paragraphs with link by marking them mer…#570
#569 fixed talkBack skipping paragraphs with link by marking them mer…#570bpappin wants to merge 2 commits into
Conversation
|
By the way @mikepenz i noticed that in TalkBack, when navigating over list items, TalkBack selects the point first, then the content as the next item. You could probably use a similar technique (or add to this) to improve how that works. |
There was a problem hiding this comment.
Pull request overview
Fixes an accessibility issue where Android TalkBack can skip reading paragraphs that contain inline links by adjusting Compose semantics for markdown text rendering.
Changes:
- Applies
semantics(mergeDescendants = true)on text segments that contain link annotations. - Conditionally wraps link-containing content in a traversal-group container to preserve TalkBack reading order.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (hasLinks) { | ||
| Box(modifier = Modifier.semantics { isTraversalGroup = true }.onPlaced { | ||
| it.parentLayoutCoordinates?.also { coordinates -> | ||
| containerSize.value = coordinates.size.toSize() | ||
| } | ||
| }) { | ||
| textSegment(content, modifier) | ||
| } | ||
| } else { | ||
| textSegment(content, modifier.onPlaced { | ||
| it.parentLayoutCoordinates?.also { coordinates -> | ||
| containerSize.value = coordinates.size.toSize() | ||
| } | ||
| }) | ||
| } |
There was a problem hiding this comment.
Replaced the custom Box modifier chain with the library's containerModifier(modifier), which preserves the provided modifiers, then pass an empty Modifier to the internal textSegment.
| val hasSegmentLinks = segment.getLinkAnnotations(0, segment.length).isNotEmpty() | ||
| val finalModifier = if (hasSegmentLinks) { | ||
| segmentDrawModifier.semantics(mergeDescendants = true) { } | ||
| } else { | ||
| segmentDrawModifier | ||
| } | ||
| MarkdownBasicText( | ||
| text = extended, | ||
| modifier = segmentDrawModifier.let { animations.animateTextSize(it) }, | ||
| modifier = finalModifier.let { animations.animateTextSize(it) }, |
There was a problem hiding this comment.
Let me see about adding more testing to this.
but...
The whole point of this change was explicitly to change the semantic structure, so that TalkBack would read the paragraph and links.
It currently skips over the whole paragraph if it has links in it.
There was a problem hiding this comment.
added new test: inline_links_are_individually_discoverable_and_readable
in: Semantics
"You can use mergeDescendants = true to indicate that a parent Composable's descendants should be merged into its semantic tree. This tells accessibility services like TalkBack to treat the Composable and all of its descendants as a single element, which helps reduce the number of focusable items on screen."
And a little help from gemini to explain it:
Why it relates to LinkAnnotation and text-skipping
- The issue: In Compose 1.7.0+,
LinkAnnotationelements automatically place individual virtual accessibility nodes into the semantics tree so that they can be read. However, because they are children of the text node in the semantics tree, TalkBack attempts to focus on them individually. When TalkBack encounters a complex mix of text and focusable child nodes (the links), it can skip reading the parent text and jump straight to the links, or focus them in an incorrect order. - The resolution: By applying
Modifier.semantics(mergeDescendants = true) {}to theTextcomposable, Compose merges these virtual child nodes into the parentTextnode. This prevents TalkBack from jumping focus inside the layout flow, forcing it to read the paragraph as a single cohesive unit. - Link Clickability: TalkBack still retains access to the links inside the merged parent text. When the merged paragraph is focused, TalkBack parses the raw
AnnotatedStringcontent and exposes all URL links inside the standard TalkBack local links menu (accessed via standard swipe gestures), allowing the user to select and trigger them.
This design pattern is standard practice in Compose development for ensuring correct accessibility ordering when rendering rich text.
…to the outer `Box` container of text segments with links, and passing `Modifier` to the internal `textSegment`. - Add the `inline_links_are_individually_discoverable_and_readable` regression test to `MarkdownA11yTest.kt` to verify that `mergeDescendants = true` does not hide paragraph text or prune link annotations.
Fix TalkBack skipping paragraphs with inline links
Description
This PR resolves an accessibility issue where screen readers (such as Android TalkBack) would skip reading entire paragraphs of text if they contained inline link annotations.
Key Changes:
MarkdownText.kt, we applysemantics { mergeDescendants = true }to the text segment modifier when link annotations are present. This forces screen readers to group and read the paragraph containing links as a cohesive text unit instead of skipping it or focusing links out of order.isTraversalGroup = trueto preserve correct layout traversal order as expected by regression tests.Note
Running the test suite locally required fixing an asynchronous timing/import issue in
MarkdownA11yTest.kt. That test fix has been excluded from this PR as it is out of scope.Fixes #569
Type of change
How Has This Been Tested?
The change has been validated against the existing test suite:
./gradlew :multiplatform-markdown-renderer-m3:jvmTestChecklist: