Skip to content

Keep a shadow's copy of its content out of the accessibility tree - #538

Open
Ceylo wants to merge 1 commit into
skiptools:mainfrom
Ceylo:fix/shadow-accessibility
Open

Ceylo wants to merge 1 commit into
skiptools:mainfrom
Ceylo:fix/shadow-accessibility

Conversation

@Ceylo

@Ceylo Ceylo commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What

Shadowed composes the content twice, the second time as the shadow, and that copy kept its semantics. Compose drops a node that a sibling above it fully covers, so a plain .shadow(radius:) hid the copy. But wherever the shadow extends past the content, the copy reached TalkBack: with an offset, a second copy of a shadowed label and an unlabelled click target under a shadowed button. This PR clears the copy's semantics with clearAndSetSemantics {}.

Before / after

Accessibility nodes from uiautomator dump, outlined on the screenshot (Fuse, API 37 emulator); red marks nodes that exist only before. Two rows added locally to the Shadow playground: Text("Shifted") and a bordered Button("Press"), each with .shadow(color: .black, radius: 4, x: 0, y: 12).

before after

In the Shadow playground as shipped, the shadowed Toggle also loses an 11 px unlabelled click target beside its switch.

Side effects

  • The shadow still draws. The Shadow playground is pixel-identical before and after, in Lite and Fuse.
  • Shadowed buttons still work: one action per tap in every arm.
  • Unchanged: the copy still takes touches. This clears semantics, not pointer input, so a tap just below an offset-shadowed button still fires it. Out of scope here.

Verified

  • swift test: 118 Swift tests, 55 skipped, 0 failures. JUnit: 162 tests, 157 passed, 5 skipped.
  • Mutation run: with main's Shadowed.kt, both new tests fail with Expected exactly '1' node but found '2' (one on the label, one on the click action); with this branch they pass.
  • Showcase built through Gradle against this branch, run in Lite and Fuse on an API 37 emulator before and after. Not run on iOS: the change is Kotlin-only.

Skip Pull Request Checklist:

  • REQUIRED: I have signed the Contributor Agreement
  • REQUIRED: I have tested my change locally with swift test
  • OPTIONAL: I have tested my change on an iOS simulator or device. Not run: the change is Kotlin-only.
  • OPTIONAL: I have tested my change on an Android emulator or device
  • REQUIRED: I have checked whether this change requires a corresponding update in the Skip Fuse UI repository (link related PR if applicable). No API change, so no paired PR.
  • OPTIONAL: I have added an example of any UI changes to the Showcase sample app. No UI change, and the Shadow playground already exercises shadowed buttons and toggles.

  • AI was used to generate or assist with generating this PR. Please specify below how you used AI to help you, and what steps you have taken to manually verify the changes.

Written with Claude Code: the fix, the tests and this description. Every claim above comes from a run: the mutation run, and the Showcase in Lite and Fuse on an emulator before and after, with node counts from the accessibility dump and a pixel comparison of the Shadow playground.

Shadowed composes the content twice, the second time as the shadow, and
that copy kept its semantics. Compose leaves out an accessibility node
where a sibling drawn above it covers it, so the copy surfaced wherever
the shadow extends past the content: with an offset, TalkBack found the
label twice and an unlabelled click target under each button. Clear the
copy's semantics.
@cla-bot cla-bot Bot added the cla-signed label Oct 2, 2026
@Ceylo
Ceylo marked this pull request as ready for review October 2, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant