Skip to content

#569 fixed talkBack skipping paragraphs with link by marking them mer…#570

Open
bpappin wants to merge 2 commits into
mikepenz:developfrom
bpappin:feature/569_TalkBack_skips_paragraphs_with_links
Open

#569 fixed talkBack skipping paragraphs with link by marking them mer…#570
bpappin wants to merge 2 commits into
mikepenz:developfrom
bpappin:feature/569_TalkBack_skips_paragraphs_with_links

Conversation

@bpappin
Copy link
Copy Markdown

@bpappin bpappin commented May 27, 2026

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:

  1. Accessibility Semantics Fix: In MarkdownText.kt, we apply semantics { 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.
  2. Outer Container Traversal Grouping: Maintained the outer container wrapping the text segment with isTraversalGroup = true to 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code style update (formatting, renaming)
  • Refactoring (no functional changes, no API changes)
  • Build configuration change
  • Other (please describe):

How Has This Been Tested?

The change has been validated against the existing test suite:

  • Test A: Executed the core and Material 3 module unit tests to ensure that the layout, structure, and accessibility semantics trees still pass regression testing:
    ./gradlew :multiplatform-markdown-renderer-m3:jvmTest
  • Manual: Manual testing was conducted using the sample app to keep a human eye int he loop.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published in downstream modules

@bpappin
Copy link
Copy Markdown
Author

bpappin commented May 28, 2026

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.
Maybe merge and apply list semantics.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +221 to +235
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()
}
})
}
Copy link
Copy Markdown
Author

@bpappin bpappin May 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +200 to +208
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) },
Copy link
Copy Markdown
Author

@bpappin bpappin May 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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+, LinkAnnotation elements 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 the Text composable, Compose merges these virtual child nodes into the parent Text node. 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 AnnotatedString content 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TalkBack skips paragraphs with links

2 participants