Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -46,7 +46,6 @@ import androidx.compose.ui.semantics.SemanticsOwner
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.getAllUncoveredSemanticsNodesToIntObjectMap
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.semantics.isImportantForAccessibility
import androidx.compose.ui.semantics.sortByGeometryGroupings
import androidx.compose.ui.uikit.density
import androidx.compose.ui.uikit.toNanoSeconds
Expand Down Expand Up @@ -200,10 +199,10 @@ private sealed interface AccessibilityNode {
* @mediator reference to the containing AccessibilityMediator
*/
class Semantics(
override val semanticsNode: SemanticsNode,
semanticsNode: SemanticsNode,
private val mediator: AccessibilityMediator,
private val isBeyondBounds: Boolean
) : AccessibilityNode {
private val isBeyondBounds: Boolean,
) : Container(semanticsNode) {
private val cachedConfig = semanticsNode.config
private val scrollableParentNodeIds by lazy { semanticsNode.allScrollableParentNodeIds }

Expand All @@ -217,6 +216,13 @@ private sealed interface AccessibilityNode {
}
}

override val accessibilityContainerType: UIAccessibilityContainerType
get() = when {
semanticsNode.canBeAccessibilityElement() -> UIAccessibilityContainerTypeNone
semanticsNode.isTraversalGroup -> UIAccessibilityContainerTypeSemanticGroup
else -> UIAccessibilityContainerTypeNone
}

override val accessibilityInteropView: InteropWrappingView?
get() = cachedConfig.getOrNull(NativeAccessibilityViewSemanticsKey)?.also {
it.isAccessibilityFocusable = ::isBeyondBoundsOrFocusable
Expand Down Expand Up @@ -357,7 +363,7 @@ private sealed interface AccessibilityNode {
* with all its children. [Container] is used to indicate element that contains container
* semantic node with all its children.
*/
class Container(
open class Container(
override val semanticsNode: SemanticsNode
) : AccessibilityNode {
override val key: AccessibilityElementKey = semanticsNode.containerKey
Expand Down Expand Up @@ -1541,7 +1547,7 @@ internal class AccessibilityMediator(
node = AccessibilityNode.Semantics(
semanticsNode = node,
mediator = this,
isBeyondBounds = isBeyondBounds
isBeyondBounds = isBeyondBounds,
),
container = container,
children = children,
Expand Down Expand Up @@ -1576,25 +1582,29 @@ internal class AccessibilityMediator(
traverseChildren(it, isBeyondBounds = true, flatten = flattenChildren, container = node)
}

val allElements = beforeElements + visibleElements + afterElements
if (node.isTraversalGroup || node.id == rootNode.id) {
val hasSemanticsNode = node.isImportantForAccessibility() ||
node.config.contains(SemanticsProperties.TestTag)

val containerChildren = if (hasSemanticsNode) {
listOf(makeSemanticsNode(allElements))
if (node.canBeAccessibilityElement()) {
val containerElement = listOf(makeSemanticsNode(emptyList()))
createOrUpdateAccessibilityElement(
node = AccessibilityNode.Container(semanticsNode = node),
container = container,
children = beforeElements + visibleElements + containerElement + afterElements,
frame = frame
)
} else {
allElements
createOrUpdateAccessibilityElement(
node = AccessibilityNode.Semantics(
semanticsNode = node,
mediator = this,
isBeyondBounds = isBeyondBounds
),
container = container,
children = beforeElements + visibleElements + afterElements,
frame = frame
)
}

createOrUpdateAccessibilityElement(
node = AccessibilityNode.Container(semanticsNode = node),
container = container,
children = containerChildren,
frame = frame
)
} else {
makeSemanticsNode(allElements)
makeSemanticsNode(beforeElements + visibleElements + afterElements)
}
} else {
makeSemanticsNode(emptyList())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ import kotlin.test.assertTrue
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
import org.jetbrains.skiko.available
import platform.UIKit.UIAccessibilityContainerTypeNone
import platform.UIKit.UIAccessibilityContainerTypeSemanticGroup
import platform.UIKit.UIAccessibilityTraitAdjustable
import platform.UIKit.UIAccessibilityTraitButton
import platform.UIKit.UIAccessibilityTraitHeader
Expand Down Expand Up @@ -1143,6 +1145,44 @@ class ComponentsAccessibilitySemanticTest {
}
}

@Test
fun testTraversalGroupWithSemantics() = runUIKitInstrumentedTest {
setContent {
Column(
modifier = Modifier.semantics {
testTag = "group_column"
isTraversalGroup = true
// Should be added as a separate accessibility element
contentDescription = "Group Column"
}
) {
Button(
onClick = {},
modifier = Modifier.semantics {
testTag = "button"
}
) {
Text("Button text")
}
}
}
assertAccessibilityTree {
isAccessibilityElement = false
containerType = UIAccessibilityContainerTypeSemanticGroup
node {
identifier = "button"
isAccessibilityElement = true
containerType = UIAccessibilityContainerTypeNone
}
node {
identifier = "group_column"
label = "Group Column"
isAccessibilityElement = true
containerType = UIAccessibilityContainerTypeNone
}
}
}

@Test
fun testMergeDescendantsWithButton() = runUIKitInstrumentedTest {
setContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ import kotlinx.cinterop.ExperimentalForeignApi
import org.jetbrains.skiko.OS
import org.jetbrains.skiko.OSVersion
import org.jetbrains.skiko.available
import platform.UIKit.UIAccessibilityContainerType
import platform.UIKit.UIAccessibilityContainerTypeDataTable
import platform.UIKit.UIAccessibilityContainerTypeLandmark
import platform.UIKit.UIAccessibilityContainerTypeList
import platform.UIKit.UIAccessibilityContainerTypeNone
import platform.UIKit.UIAccessibilityContainerTypeSemanticGroup
import platform.UIKit.UIAccessibilityElement
import platform.UIKit.UIAccessibilityTraitAdjustable
import platform.UIKit.UIAccessibilityTraitAllowsDirectInteraction
Expand All @@ -57,6 +63,7 @@ import platform.UIKit.UIAccessibilityTraits
import platform.UIKit.UIView
import platform.UIKit.UIWindow
import platform.UIKit.UIWindowScene
import platform.UIKit.accessibilityContainerType
import platform.UIKit.accessibilityCustomActions
import platform.UIKit.accessibilityElementAtIndex
import platform.UIKit.accessibilityElementCount
Expand Down Expand Up @@ -142,6 +149,7 @@ internal fun UIKitInstrumentedTest.getAccessibilityTree(): AccessibilityTestNode
label = element.accessibilityLabel,
value = element.accessibilityValue,
frame = element.accessibilityFrame.asDpRect(),
containerType = element.accessibilityContainerType,
children = children,
traits = allAccessibilityTraits.keys.filter {
element.accessibilityTraits and it != 0.toULong()
Expand Down Expand Up @@ -184,6 +192,14 @@ private val allAccessibilityTraits = mutableMapOf(
it as Map<UIAccessibilityTraits, String>
}

private val allContainerTypes = mapOf(
UIAccessibilityContainerTypeNone to "UIAccessibilityContainerTypeNone",
UIAccessibilityContainerTypeDataTable to "UIAccessibilityContainerTypeDataTable",
UIAccessibilityContainerTypeList to "UIAccessibilityContainerTypeList",
UIAccessibilityContainerTypeLandmark to "UIAccessibilityContainerTypeLandmark",
UIAccessibilityContainerTypeSemanticGroup to "UIAccessibilityContainerTypeSemanticGroup",
)

/**
* Represents a node in an accessibility tree, which is used for testing accessibility features
* within a UI hierarchy. This class captures various accessibility properties of UI components
Expand All @@ -195,6 +211,7 @@ internal data class AccessibilityTestNode(
var label: String? = null,
var value: String? = null,
var frame: DpRect? = null,
var containerType: UIAccessibilityContainerType? = null,
var children: List<AccessibilityTestNode>? = null,
var traits: List<UIAccessibilityTraits>? = null,
var element: NSObject? = null,
Expand Down Expand Up @@ -227,6 +244,9 @@ internal data class AccessibilityTestNode(
traits?.let {
assertEquals(it.toSet(), actualNode?.traits?.toSet())
}
containerType?.let {
assertEquals(it, actualNode?.containerType)
}
children?.let {
assertEquals(it.count(), actualNode?.children?.count())
it.zip(actualNode?.children ?: emptyList()) { validator, child ->
Expand Down Expand Up @@ -268,6 +288,10 @@ internal data class AccessibilityTestNode(
builder.appendLine("$fieldIndent - ${allAccessibilityTraits.getValue(it)}")
}
}
node.containerType?.takeIf { it != UIAccessibilityContainerTypeNone }?.let {
val typeString = allContainerTypes[it] ?: "Unknown: $it"
builder.appendLine("$fieldIndent accessibilityContainerType: $typeString")
}
node.value?.let { builder.appendLine("$fieldIndent accessibilityValue: $it") }
node.element?.accessibilityCustomActions?.takeIf { it.isNotEmpty() }?.let {
builder.appendLine("$fieldIndent accessibilityCustomActions: $it")
Expand Down
Loading