Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ internal class ComposeAccessible(
}

override fun getAccessibleName(): String? {
// For text fields, prioritize contentDescription as the accessible name (label),
// since the text content is available through AccessibleText/AccessibleEditableText.
// This matches iOS behavior where accessibilityLabel = label and accessibilityValue = content.
if (setText != null) {
return semanticsConfig
.getOrNull(SemanticsProperties.ContentDescription)
?.mergeText()
?: text?.toString()
}
Copy link

Choose a reason for hiding this comment

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

Is there a reason we should not just always return

(semanticsConfig.getOrNull(SemanticsProperties.Text) ?:  semanticsConfig.getOrNull(SemanticsProperties.ContentDescription))
    ?.mergeText()

?

Copy link

Choose a reason for hiding this comment

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

i.e., why only if setText != null?

Copy link
Author

Choose a reason for hiding this comment

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

The setText != null check is necessary because Text ?: ContentDescription doesn't work for the fallback case.

I tested your suggestion and it fails the textFieldAccessibleNameFallsBackToTextContent test. The issue is that for TextFields, SemanticsProperties.Text may exist in the config but doesn't provide the actual text content—that comes from the text field (via AccessibleText).

So when there's no contentDescription, we need text?.toString() as fallback, not SemanticsProperties.Text.

Copy link

Choose a reason for hiding this comment

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

Err, sorry, I meant the other way around (ContentDescription ?: Text).

My point was that it seems to me the content description should be used (if set) not just for text fields (or editable text components in general).

Copy link
Author

@kdroidFilter kdroidFilter Jan 13, 2026

Choose a reason for hiding this comment

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

I understand your point—using ContentDescription ?: Text for all components would be more consistent.

However, this fails the textFieldAccessibleNameFallsBackToTextContent test because SemanticsProperties.Text is null/empty for editable TextFields.

The fallback needs text?.toString() (from the Java AccessibleText API), not SemanticsProperties.Text (from Compose semantics). The setText != null check ensures we use the correct fallback source specifically for text fields.

Copy link

Choose a reason for hiding this comment

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

My question wasn't about semanticsConfig.getOrNull(SemanticsProperties.Text) vs. text (which is just a calculated property of ComposeAccessibleComponent, by the way; it doesn't come from the Java API), although that's a valid question too. It was about using ContentDescription even if setText is null. Is there a reason not to do that?

About the question of semanticsConfig.getOrNull(SemanticsProperties.Text) vs. text (which itself is EditableText ?: Text): I don't think it's correct to use EditableText in getAccessibleName (or getAccessibleDescription.

So my suggestion is:

  • In getAccessibleName return ContentDescription ?: Text.
  • In getAccessibleDescription return ContentDescription.

Do you know of any cases where this would result in undesirable behavior from the OS accessibility system?

Copy link

Choose a reason for hiding this comment

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

Hmm, getAccessibleDescription already returns ContentDescription.

So just the first suggestion then.

Copy link
Author

Choose a reason for hiding this comment

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

Ah, sorry, I hadn’t understood it properly.

On my side, I also didn’t fully understand why EditableText was being used in getAccessibleName, but I intentionally kept it to avoid introducing overly large changes. My goal was really to fix this specific bug, without further altering the existing behavior.

To my knowledge, this should not cause any issues with screen readers: they should be able to read the content correctly through the AccessibleText interface.

Would you like me to apply the proposed changes?

Copy link

Choose a reason for hiding this comment

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

Would you like me to apply the proposed changes?

Yes, let's do that, and if/when anyone complains we'll fix it (and document exactly why).

Copy link
Author

Choose a reason for hiding this comment

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

I've implemented your suggestion.
However, when running all AccessibilityTest tests together, 10 tests fail with assert(activeControllers.isEmpty()) in the cleanup. These same tests pass when run individually.

return text?.toString()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,39 @@ class AccessibilityTest {
.isEqualTo(AccessibleRole.PUSH_BUTTON)
}

@Test
fun textFieldAccessibleNameUsesContentDescription() = runDesktopA11yTest {
test.setContent {
BasicTextField(
value = "typed text",
onValueChange = { },
modifier = Modifier
.testTag("textFieldWithLabel")
.semantics {
contentDescription = "Email"
}
)
}

val context = test.onNodeWithTag("textFieldWithLabel").fetchAccessible().accessibleContext
assertThat(context?.accessibleName).isEqualTo("Email")
assertThat(context?.accessibleDescription).isEqualTo("Email")
}

@Test
fun textFieldAccessibleNameFallsBackToTextContent() = runDesktopA11yTest {
test.setContent {
BasicTextField(
value = "typed text",
onValueChange = { },
modifier = Modifier.testTag("textFieldNoLabel")
)
}

val context = test.onNodeWithTag("textFieldNoLabel").fetchAccessible().accessibleContext
assertThat(context?.accessibleName).isEqualTo("typed text")
}

private fun verifyA11yHierarchyFromAccessible(
window: Window,
@Suppress("SameParameterValue") accessibleName: String
Expand Down