Refactor semantics traversal order for more accurate top bars

This change uses 1) the idea of structural significance to determine groups of nodes to visit at a time and 2) row groupings to have more accurate traversal order.

For each node examined, if it is structurally significant, then all of its children must be placed in the final order list before moving on; if the node is not, then all of its children and its children’s children are extracted, then compared with the grouping algorithm. A structurally significant node for now is determined by if it is a container (Surface), scrollable, or a collection.

This fix is mainly focused on addressing top bar ordering issues. The tests added primarily focus on different types of top bars (TopAppBars, Scaffold top bars, top bars with scrolling columns, etc.). New tests were added to `AndroidAccessibilityTest` instead of `SemanticsTests` since the new traversal order is set via ANI properties and needs the ANI provider to test.

In addition to those changes, we no longer need semantic node’s `replacedChildrenSortedByBounds` since all previous sorting has been replaced, so that function, the `sortByBounds` parameter, and the `SemanticsSort` file has been removed.

Tests in `SemanticsTests` that were related to ordering (tests that started with `testChildrenSortedByBounds`) were removed, but new tests with the same coverage were added to `AndroidAccessibilityTest` in their place.

See go/focus-order-changes for more details.

Test: compose/ui/AndroidAccessibilityTest/testCreateAccessibilityNodeInfo_forTraversalOrder_SimpleTopAppBar and others
Fix: b/264431330
Change-Id: I419ae4feec1b319786a6cc32c883d92cb8bac466
diff --git a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/Demos.kt b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/Demos.kt
index ae58b8a..044a7ff 100644
--- a/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/Demos.kt
+++ b/compose/integration-tests/demos/src/main/java/androidx/compose/integration/demos/Demos.kt
@@ -24,6 +24,7 @@
 import androidx.compose.material.demos.MaterialDemos
 import androidx.compose.material3.demos.Material3Demos
 import androidx.compose.ui.demos.CoreDemos
+import androidx.compose.ui.demos.AccessibilityDemos
 import androidx.navigation.compose.demos.NavigationDemos
 
 /**
@@ -39,6 +40,7 @@
         MaterialDemos,
         Material3Demos,
         NavigationDemos,
-        TextDemos
+        TextDemos,
+        AccessibilityDemos
     )
 )
\ No newline at end of file
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
index a77b01b..db30658 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/UiDemos.kt
@@ -233,10 +233,14 @@
     )
 )
 
-private val AccessibilityDemos = DemoCategory(
+val AccessibilityDemos = DemoCategory(
     "Accessibility",
     listOf(
-        ComposableDemo("Overlaid Nodes") { OverlaidNodeLayoutDemo() }
+        ComposableDemo("Scaffold Top Bar") { ScaffoldSample() },
+        ComposableDemo("Scaffold with Scrolling") { ScaffoldSampleScroll() },
+        ComposableDemo("Simple Top Bar with Scrolling") { ScrollingColumnDemo() },
+        ComposableDemo("Nested Containers—True") { NestedContainersTrueDemo() },
+        ComposableDemo("Nested Containers—False") { NestedContainersFalseDemo() }
     )
 )
 
diff --git a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
index 6e36aee..9d4725fe 100644
--- a/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
+++ b/compose/ui/ui/integration-tests/ui-demos/src/main/java/androidx/compose/ui/demos/accessibility/ComplexAccessibility.kt
@@ -16,13 +16,33 @@
 
 package androidx.compose.ui.demos
 
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomAppBar
+import androidx.compose.material.DrawerValue
+import androidx.compose.material.FabPosition
+import androidx.compose.material.FloatingActionButton
+import androidx.compose.material.Icon
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Scaffold
 import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.rememberDrawerState
+import androidx.compose.material.rememberScaffoldState
 import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.semantics.isContainer
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.compose.ui.unit.dp
 
@@ -58,7 +78,7 @@
 fun OverlaidNodeLayoutDemo() {
     LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
         Row {
-            Column {
+            Column(modifier = Modifier.testTag("Text1")) {
                 Row { Text("text1\n") }
                 Row { Text("text2\n") }
                 Row { Text("text3\n") }
@@ -69,3 +89,151 @@
         }
     }
 }
+
+@Composable
+fun CardRow(
+    modifier: Modifier,
+    columnNumber: Int,
+    topSampleText: String,
+    bottomSampleText: String
+) {
+    Row(
+        modifier,
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.End
+    ) {
+        Column {
+            Text(topSampleText + columnNumber)
+            Text(bottomSampleText + columnNumber)
+        }
+    }
+}
+
+@Preview
+@Composable
+fun NestedContainersFalseDemo() {
+    var topSampleText = "Top text in column "
+    var bottomSampleText = "Bottom text in column "
+    Column(
+        Modifier
+            .testTag("Test Tag")
+            .semantics { isContainer = true }
+    ) {
+        Row() { Modifier.semantics { isContainer = true }
+            CardRow(
+                Modifier.semantics { isContainer = false },
+                1,
+                topSampleText,
+                bottomSampleText)
+            CardRow(
+                Modifier.semantics { isContainer = false },
+                2,
+                topSampleText,
+                bottomSampleText)
+        }
+    }
+}
+
+@Preview
+@Composable
+fun NestedContainersTrueDemo() {
+    var topSampleText = "Top text in column "
+    var bottomSampleText = "Bottom text in column "
+    Column(
+        Modifier
+            .testTag("Test Tag")
+            .semantics { isContainer = true }
+    ) {
+        Row() { Modifier.semantics { isContainer = true }
+            CardRow(
+                Modifier.semantics { isContainer = true },
+                1,
+                topSampleText,
+                bottomSampleText)
+            CardRow(
+                Modifier.semantics { isContainer = true },
+                2,
+                topSampleText,
+                bottomSampleText)
+        }
+    }
+}
+
+@Composable
+fun TopAppBar() {
+    val topAppBar = "Top App Bar"
+    TopAppBar(
+        title = {
+            Text(text = topAppBar)
+        }
+    )
+}
+
+@Composable
+fun ScrollColumn(padding: PaddingValues) {
+    var counter = 0
+    var sampleText = "Sample text in column"
+    Column(
+        Modifier
+            .verticalScroll(rememberScrollState())
+            .padding(padding)
+            .testTag("Test Tag")
+    ) {
+        repeat(100) {
+            Text(sampleText + counter++)
+        }
+    }
+}
+
+@Preview
+@Composable
+fun ScaffoldSample() {
+    val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
+    Scaffold(
+        scaffoldState = scaffoldState,
+        topBar = { TopAppBar() },
+        floatingActionButtonPosition = FabPosition.End,
+        floatingActionButton = { FloatingActionButton(onClick = {}) {
+            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
+        } },
+        drawerContent = { Text(text = "Drawer Menu 1") },
+        content = { padding -> Text("Content", modifier = Modifier.padding(padding)) },
+        bottomBar = { BottomAppBar(backgroundColor = MaterialTheme.colors.primary) {
+            Text("Bottom App Bar") } }
+    )
+}
+
+@Preview
+@Composable
+fun ScaffoldSampleScroll() {
+    val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
+    Scaffold(
+        scaffoldState = scaffoldState,
+        topBar = { TopAppBar() },
+        floatingActionButtonPosition = FabPosition.End,
+        floatingActionButton = { FloatingActionButton(onClick = {}) {
+            Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
+        } },
+        content = { padding -> ScrollColumn(padding) },
+        bottomBar = { BottomAppBar(backgroundColor = MaterialTheme.colors.primary) {
+            Text("Bottom App Bar") } }
+    )
+}
+
+@Preview
+@Composable
+fun ScrollingColumnDemo() {
+    var sampleText = "Sample text in column"
+    var counter = 0
+
+    Column(
+        Modifier
+            .verticalScroll(rememberScrollState())
+            .testTag("Test Tag")
+    ) {
+        TopAppBar()
+        repeat(100) {
+            Text(sampleText + counter++)
+        }
+    }
+}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
index 1e844e4..c931504 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidAccessibilityTest.kt
@@ -47,6 +47,7 @@
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.gestures.scrollBy
 import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Row
@@ -65,13 +66,22 @@
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomAppBar
+import androidx.compose.material.DrawerValue
 import androidx.compose.material.DropdownMenu
 import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.FabPosition
+import androidx.compose.material.FloatingActionButton
 import androidx.compose.material.Icon
 import androidx.compose.material.IconButton
+import androidx.compose.material.Scaffold
 import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.rememberDrawerState
+import androidx.compose.material.rememberScaffoldState
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.SideEffect
@@ -89,12 +99,14 @@
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.graphics.toComposeRect
 import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.platform.AndroidComposeView
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.ClassName
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.InvalidId
 import androidx.compose.ui.platform.AndroidComposeViewAccessibilityDelegateCompat.Companion.TextFieldClassName
 import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.LocalView
 import androidx.compose.ui.platform.getAllUncoveredSemanticsNodesToMap
 import androidx.compose.ui.platform.testTag
@@ -106,6 +118,7 @@
 import androidx.compose.ui.semantics.contentDescription
 import androidx.compose.ui.semantics.getOrNull
 import androidx.compose.ui.semantics.invisibleToUser
+import androidx.compose.ui.semantics.isContainer
 import androidx.compose.ui.semantics.paneTitle
 import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
@@ -140,6 +153,9 @@
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
 import androidx.compose.ui.viewinterop.AndroidView
 import androidx.compose.ui.window.Dialog
 import androidx.core.view.ViewCompat
@@ -174,6 +190,7 @@
 import org.mockito.ArgumentMatchers.any
 import org.mockito.internal.matchers.apachecommons.ReflectionEquals
 import java.lang.reflect.Method
+import kotlin.math.max
 
 @LargeTest
 @RunWith(AndroidJUnit4::class)
@@ -639,7 +656,7 @@
     }
 
     @Test
-    fun testCreateAccessibilityNodeInfo_forTraversalBefore_layout() {
+    fun testSortedAccessibilityNodeInfo_forTraversalBefore_overlaidNodeLayout() {
         val overlaidText = "Overlaid node text"
         val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
         val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
@@ -669,11 +686,11 @@
         // comparison (like SemanticsSort), the third text node should come before the overlaid node
         // — OverlaidNode should be read last
         assertNotEquals(ani3TraversalBeforeVal, 0)
-        assertEquals(ani3TraversalBeforeVal!!, overlaidNode.id)
+        assertEquals(ani3TraversalBeforeVal, overlaidNode.id)
     }
 
     @Test
-    fun testCreateAccessibilityNodeInfo_forTraversalAfter_layout() {
+    fun testSortedAccessibilityNodeInfo_forTraversalAfter_overlaidNodeLayout() {
         val overlaidText = "Overlaid node text"
         val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
         val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
@@ -704,47 +721,729 @@
         // comparison (like SemanticsSort), the third text node should come before the overlaid node
         // — OverlaidNode should be read last
         assertNotEquals(overlaidTraversalAfterValue, 0)
-        assertEquals(overlaidTraversalAfterValue!!, node3.id)
+        assertEquals(overlaidTraversalAfterValue, node3.id)
+    }
+
+    @Composable
+    fun CardRow(
+        modifier: Modifier,
+        columnNumber: Int,
+        topSampleText: String,
+        bottomSampleText: String
+    ) {
+        Row(
+            modifier,
+            verticalAlignment = Alignment.CenterVertically,
+            horizontalArrangement = Arrangement.End
+        ) {
+            Column {
+                Text(topSampleText + columnNumber)
+                Text(bottomSampleText + columnNumber)
+            }
+        }
     }
 
     @Test
-    fun testCreateAccessibilityNodeInfo_forTraversalBefore_layoutTestTags() {
-        val overlaidText = "Overlaid node text"
-        val text1 = "Lorem1 ipsum dolor sit amet, consectetur adipiscing elit.\n"
-        val text2 = "Lorem2 ipsum dolor sit amet, consectetur adipiscing elit.\n"
-        val text3 = "Lorem3 ipsum dolor sit amet, consectetur adipiscing elit.\n"
+    fun testSortedAccessibilityNodeInfo_nestedContainers_outerFalse() {
+        var topSampleText = "Top text in column "
+        var bottomSampleText = "Bottom text in column "
         container.setContent {
-            LastElementOverLaidColumn(modifier = Modifier.padding(8.dp)) {
-                Row(modifier = Modifier
-                    .semantics(true) { contentDescription = "Row1" }
-                    .testTag("Row1")
-                ) {
-                    Column(modifier = Modifier.testTag("Column1")) {
-                        Row(modifier = Modifier.testTag("Text1")) { Text(text1) }
-                        Row(modifier = Modifier.testTag("Text2")) { Text(text2) }
-                        Row(modifier = Modifier.testTag("Text3")) { Text(text3) }
-                    }
-                }
-                Row(modifier = Modifier
-                    .semantics(true) { contentDescription = "Row2" }
-                    .testTag("Row2")
-                ) {
-                    Text(overlaidText)
+            Column(
+                Modifier
+                    .testTag("Test Tag")
+                    .semantics { isContainer = false }
+            ) {
+                Row() { Modifier.semantics { isContainer = false }
+                    CardRow(
+                        Modifier.semantics { isContainer = true },
+                        1,
+                        topSampleText,
+                        bottomSampleText)
+                    CardRow(
+                        Modifier.semantics { isContainer = true },
+                        2,
+                        topSampleText,
+                        bottomSampleText)
                 }
             }
         }
 
-        val node3 = rule.onNodeWithText(text3).fetchSemanticsNode()
-        val row2 = rule.onNodeWithTag("Row2").fetchSemanticsNode()
+        val topText1 = rule.onNodeWithText(topSampleText + 1).fetchSemanticsNode()
+        val topText2 = rule.onNodeWithText(topSampleText + 2).fetchSemanticsNode()
+        val bottomText1 = rule.onNodeWithText(bottomSampleText + 1).fetchSemanticsNode()
+        val bottomText2 = rule.onNodeWithText(bottomSampleText + 2).fetchSemanticsNode()
 
-        val ani3 = provider.createAccessibilityNodeInfo(node3.id)
-        val ani3TraversalBeforeVal = ani3?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val topText1ANI = provider.createAccessibilityNodeInfo(topText1.id)
+        val topText2ANI = provider.createAccessibilityNodeInfo(topText2.id)
 
-        // Nodes 1, 2, and 3 are all children of a larger column; this means with a hierarchy
-        // comparison (like SemanticsSort), the third text node should come before the overlaid node
-        // — OverlaidNode and its row should be read last
-        assertNotEquals(ani3TraversalBeforeVal, 0)
-        assertTrue(ani3TraversalBeforeVal!! < row2.id)
+        val topText1Before = topText1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val topText2Before = topText2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Here we have the following hierarchy of containers:
+        // `isContainer = false`
+        //    `isContainer = false`
+        //       `isContainer = true`
+        //       `isContainer = true`
+        // meaning the behavior should be as if the first two `isContainer = false` are not present
+        // and all of column 1 should be read before column 2.
+        assertEquals(topText1Before, bottomText1.id)
+        assertEquals(topText2Before, bottomText2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_nestedContainers_outerTrue() {
+        var topSampleText = "Top text in column "
+        var bottomSampleText = "Bottom text in column "
+        container.setContent {
+            Column(
+                Modifier
+                    .testTag("Test Tag")
+                    .semantics { isContainer = true }
+            ) {
+                Row() { Modifier.semantics { isContainer = true }
+                    CardRow(
+                        Modifier
+                            .testTag("Row 1")
+                            .semantics { isContainer = false },
+                        1,
+                        topSampleText,
+                        bottomSampleText)
+                    CardRow(
+                        Modifier
+                            .testTag("Row 2")
+                            .semantics { isContainer = false },
+                        2,
+                        topSampleText,
+                        bottomSampleText)
+                }
+            }
+        }
+
+        val bottomText1 = rule.onNodeWithText(bottomSampleText + 1).fetchSemanticsNode()
+        val bottomText2 = rule.onNodeWithText(bottomSampleText + 2).fetchSemanticsNode()
+
+        val bottomText1ANI = provider.createAccessibilityNodeInfo(bottomText1.id)
+        val bottomText1Before = bottomText1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Here we have the following hierarchy of containers:
+        // `isContainer = true`
+        //    `isContainer = true`
+        //       `isContainer = false`
+        //       `isContainer = false`
+        // In this case, we expect all the top text to be read first, then all the bottom text
+        assertEquals(bottomText1Before, bottomText2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_tripleNestedContainers() {
+        var topSampleText = "Top "
+        var bottomSampleText = "Bottom "
+        container.setContent {
+            Row {
+                CardRow(
+                    Modifier.semantics { isContainer = false },
+                    1,
+                    topSampleText,
+                    bottomSampleText)
+                CardRow(
+                    Modifier.semantics { isContainer = false },
+                    2,
+                    topSampleText,
+                    bottomSampleText)
+                CardRow(
+                    Modifier.semantics { isContainer = true },
+                    3,
+                    topSampleText,
+                    bottomSampleText)
+            }
+        }
+
+        val bottomText1 = rule.onNodeWithText(bottomSampleText + 1).fetchSemanticsNode()
+        val bottomText2 = rule.onNodeWithText(bottomSampleText + 2).fetchSemanticsNode()
+        val bottomText3 = rule.onNodeWithText(bottomSampleText + 3).fetchSemanticsNode()
+        val topText3 = rule.onNodeWithText(topSampleText + 3).fetchSemanticsNode()
+
+        val bottomText1ANI = provider.createAccessibilityNodeInfo(bottomText1.id)
+        val bottomText1Before = bottomText1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        val topText3ANI = provider.createAccessibilityNodeInfo(topText3.id)
+        val topText3Before = topText3ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Here we have the following hierarchy of containers:
+        // `isContainer = false`
+        // `isContainer = false`
+        // `isContainer = true`
+        // In this case, we expect to read in the order of: Top 1, Top 2, Bottom 1, Bottom 2,
+        // then Top 3, Bottom 3. The first two containers are effectively merged since they are both
+        // set to false, while the third container is structurally significant.
+        assertEquals(bottomText1Before, bottomText2.id)
+        assertEquals(topText3Before, bottomText3.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_nestedContainers_hierarchy() {
+        var topSampleText = "Top text in column "
+        var bottomSampleText = "Bottom text in column "
+
+        container.setContent {
+            Row {
+                CardRow(
+                    Modifier
+                        // adding a vertical scroll here makes the column scrollable, which would
+                        // normally make it structurally significant
+                        .verticalScroll(rememberScrollState())
+                        // but adding in `container = false` should negate that
+                        .semantics { isContainer = false },
+                    1,
+                    topSampleText,
+                    bottomSampleText
+                )
+                CardRow(
+                    Modifier
+                        // adding a vertical scroll here makes the column scrollable, which would
+                        // normally make it structurally significant
+                        .verticalScroll(rememberScrollState())
+                        // but adding in `container = false` should negate that
+                        .semantics { isContainer = false },
+                    2,
+                    topSampleText,
+                    bottomSampleText
+                )
+            }
+        }
+
+        val bottomText1 = rule.onNodeWithText(bottomSampleText + 1).fetchSemanticsNode()
+        val bottomText2 = rule.onNodeWithText(bottomSampleText + 2).fetchSemanticsNode()
+
+        val bottomText1ANI = provider.createAccessibilityNodeInfo(bottomText1.id)
+        val bottomText1Before = bottomText1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // In this case, we expect all the top text to be read first, then all the bottom text
+        assertEquals(bottomText1Before, bottomText2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_SimpleTopAppBar() {
+        val topAppBarText = "Top App Bar"
+        val textBoxTag = "Text Box"
+        container.setContent {
+            Box(Modifier.testTag(textBoxTag)) {
+                Text(text = "Lorem ipsum ".repeat(200))
+            }
+
+            TopAppBar(
+                title = {
+                    Text(text = topAppBarText)
+                }
+            )
+        }
+
+        val textBoxNode = rule.onNodeWithTag(textBoxTag).fetchSemanticsNode()
+        val topAppBarNode = rule.onNodeWithText(topAppBarText).fetchSemanticsNode()
+
+        val topAppBarANI = provider.createAccessibilityNodeInfo(topAppBarNode.id)
+        val topAppTraverseBefore = topAppBarANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        assertThat(topAppTraverseBefore).isLessThan(textBoxNode.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_SimpleScrollingTopAppBar() {
+        val topAppBarText = "Top App Bar"
+        val sampleText = "Sample text "
+        val sampleText1 = "Sample text 1"
+        val sampleText2 = "Sample text 2"
+        var counter = 1
+
+        container.setContent {
+            Column(
+                Modifier
+                    .verticalScroll(rememberScrollState())
+            ) {
+                TopAppBar(title = { Text(text = topAppBarText) })
+                repeat(100) {
+                    Text(sampleText + counter++)
+                }
+            }
+        }
+
+        val topAppBarNode = rule.onNodeWithText(topAppBarText).fetchSemanticsNode()
+        val topAppBarANI = provider.createAccessibilityNodeInfo(topAppBarNode.id)
+        val topAppTraverseBefore = topAppBarANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        val node1 = rule.onNodeWithText(sampleText1).fetchSemanticsNode()
+        val ANI1 = provider.createAccessibilityNodeInfo(node1.id)
+        val traverseBefore1 = ANI1?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        val node2 = rule.onNodeWithText(sampleText2).fetchSemanticsNode()
+
+        // Assert that the top bar comes before the first node (node 1) and that the first node
+        // comes before the second (node 2)
+        assertEquals(topAppTraverseBefore, node1.id)
+        assertEquals(traverseBefore1, node2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_ScaffoldTopBar() {
+        val topAppBarText = "Top App Bar"
+        val contentText = "Content"
+        val bottomAppBarText = "Bottom App Bar"
+        container.setContent {
+            val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
+            Scaffold(
+                scaffoldState = scaffoldState,
+                topBar = { TopAppBar(title = { Text(topAppBarText) }) },
+                floatingActionButtonPosition = FabPosition.End,
+                floatingActionButton = { FloatingActionButton(onClick = {}) {
+                    Icon(imageVector = Icons.Default.Add, contentDescription = "fab icon")
+                } },
+                drawerContent = { Text(text = "Drawer Menu 1") },
+                content = { padding -> Text(contentText, modifier = Modifier.padding(padding)) },
+                bottomBar = { BottomAppBar {
+                    Text(bottomAppBarText) } }
+            )
+        }
+        val topAppBarNode = rule.onNodeWithText(topAppBarText).fetchSemanticsNode()
+        val contentNode = rule.onNodeWithText(contentText).fetchSemanticsNode()
+        val bottomAppBarNode = rule.onNodeWithText(bottomAppBarText).fetchSemanticsNode()
+
+        val topAppBarANI = provider.createAccessibilityNodeInfo(topAppBarNode.id)
+        val contentANI = provider.createAccessibilityNodeInfo(contentNode.id)
+        val topAppTraverseBefore = topAppBarANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val contentTraverseBefore = contentANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        assertEquals(topAppTraverseBefore, contentNode.id)
+        assertThat(contentTraverseBefore).isLessThan(bottomAppBarNode.id)
+    }
+
+    @Composable
+    fun ScrollColumn(testTag: String) {
+        var counter = 0
+        var sampleText = "Sample text in column"
+        Column(
+            Modifier
+                .verticalScroll(rememberScrollState())
+                .testTag(testTag)
+        ) {
+            repeat(100) {
+                Text(sampleText + counter++)
+            }
+        }
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_ScaffoldScrollingTopBar() {
+        val topAppBarText = "Top App Bar"
+        val contentText = "Content"
+        val bottomAppBarText = "Bottom App Bar"
+        val fabIconDescription = "fab icon"
+
+        container.setContent {
+            val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
+            Scaffold(
+                scaffoldState = scaffoldState,
+                topBar = { TopAppBar(title = { Text(topAppBarText) }) },
+                floatingActionButtonPosition = FabPosition.End,
+                floatingActionButton = { FloatingActionButton(onClick = {}) {
+                    Icon(imageVector = Icons.Default.Add, contentDescription = fabIconDescription)
+                } },
+                drawerContent = { Text(text = "Drawer Menu 1") },
+                content = { ScrollColumn(contentText) },
+                bottomBar = { BottomAppBar {
+                    Text(bottomAppBarText) } }
+            )
+        }
+
+        val topAppBarNode = rule.onNodeWithText(topAppBarText).fetchSemanticsNode()
+        val contentNode = rule.onNodeWithTag(contentText).fetchSemanticsNode()
+        val bottomAppBarNode = rule.onNodeWithText(bottomAppBarText).fetchSemanticsNode()
+
+        val topAppBarANI = provider.createAccessibilityNodeInfo(topAppBarNode.id)
+        val contentANI = provider.createAccessibilityNodeInfo(contentNode.id)
+
+        val topAppTraverseBefore = topAppBarANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val contentTraverseBefore = contentANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        assertThat(topAppTraverseBefore).isLessThan(contentNode.id)
+        assertThat(contentTraverseBefore).isLessThan(bottomAppBarNode.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_vertical_zIndex() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        container.setContent {
+            Column(Modifier.testTag(rootTag)) {
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .zIndex(1f)
+                        .testTag(childTag1)
+                ) {}
+                SimpleTestLayout(
+                    Modifier.requiredSize(50.dp).testTag(childTag2)
+                ) {}
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child1TraverseBefore = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child2TraverseAfter = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child1 to come before child2
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child1TraverseBefore).isLessThan(child2.id)
+        assertThat(child2TraverseAfter).isLessThan(child1.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_horizontal_zIndex() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        container.setContent {
+            Row(
+                Modifier.testTag(rootTag)
+            ) {
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .zIndex(1f)
+                        .testTag(childTag1)
+                ) {}
+                SimpleTestLayout(
+                    Modifier.requiredSize(50.dp).testTag(childTag2)
+                ) {}
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child1TraverseBefore = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child2TraverseAfter = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child1 to come before child2
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child1TraverseBefore).isLessThan(child2.id)
+        assertThat(child2TraverseAfter).isLessThan(child1.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_vertical_offset() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        container.setContent {
+            Box(
+                Modifier.testTag(rootTag)
+            ) {
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .offset(x = 0.dp, y = 50.dp)
+                        .testTag(childTag1)
+                ) {}
+                SimpleTestLayout(
+                    Modifier.requiredSize(50.dp).testTag(childTag2)
+                ) {}
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child2TraverseBefore = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child1TraverseAfter = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child2 to come before child1
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child2TraverseBefore).isLessThan(child1.id)
+        assertThat(child1TraverseAfter).isLessThan(child2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_horizontal_offset() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        container.setContent {
+            Box(
+                Modifier.testTag(rootTag)
+            ) {
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .offset(x = 50.dp, y = 0.dp)
+                        .testTag(childTag1)
+                ) {}
+                SimpleTestLayout(
+                    Modifier.requiredSize(50.dp).testTag(childTag2)
+                ) {}
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child2TraverseBefore = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child1TraverseAfter = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child2 to come before child1
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child2TraverseBefore).isLessThan(child1.id)
+        assertThat(child1TraverseAfter).isLessThan(child2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_vertical_offset_overlapped() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        container.setContent {
+            Box(
+                Modifier.testTag(rootTag)
+            ) {
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .offset(x = 0.dp, y = 20.dp)
+                        .testTag(childTag1)
+                ) {}
+                SimpleTestLayout(
+                    Modifier.requiredSize(50.dp).testTag(childTag2)
+                ) {}
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child2TraverseBefore = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child1TraverseAfter = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child2 to come before child1
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child2TraverseBefore).isLessThan(child1.id)
+        assertThat(child1TraverseAfter).isLessThan(child2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_horizontal_offset_overlapped() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        container.setContent {
+            Box(
+                Modifier.testTag(rootTag)
+            ) {
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .offset(x = 20.dp, y = 0.dp)
+                        .testTag(childTag1)
+                ) {}
+                SimpleTestLayout(
+                    Modifier
+                        .requiredSize(50.dp)
+                        .offset(x = 0.dp, y = 20.dp)
+                        .testTag(childTag2)
+                ) {}
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child2TraverseBefore = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child1TraverseAfter = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child2 to come before child1
+        assertEquals(2, root.replacedChildren.size)
+        assertEquals(child2TraverseBefore, child1.id)
+        assertEquals(child1TraverseAfter, child2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_vertical_subcompose() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        val density = Density(1f)
+        val size = with(density) { 100.dp.roundToPx() }.toFloat()
+        container.setContent {
+            CompositionLocalProvider(LocalDensity provides density) {
+                SimpleSubcomposeLayout(
+                    Modifier.testTag(rootTag),
+                    {
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(childTag1)
+                        ) {}
+                    },
+                    Offset(0f, size),
+                    {
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(childTag2)
+                        ) {}
+                    },
+                    Offset(0f, 0f)
+                )
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child2TraverseBefore = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child1TraverseAfter = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child2 to come before child1
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child2TraverseBefore).isLessThan(child1.id)
+        assertThat(child1TraverseAfter).isLessThan(child2.id)
+    }
+
+    @Test
+    fun testSortedAccessibilityNodeInfo_horizontal_subcompose() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        val density = Density(1f)
+        val size = with(density) { 100.dp.roundToPx() }.toFloat()
+        container.setContent {
+            CompositionLocalProvider(LocalDensity provides density) {
+                SimpleSubcomposeLayout(
+                    Modifier.testTag(rootTag),
+                    {
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(childTag1)
+                        ) {}
+                    },
+                    Offset(size, 0f),
+                    {
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(childTag2)
+                        ) {}
+                    },
+                    Offset(0f, 0f)
+                )
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        val child1 = rule.onNodeWithTag(childTag1).fetchSemanticsNode()
+        val child2 = rule.onNodeWithTag(childTag2).fetchSemanticsNode()
+
+        val child1ANI = provider.createAccessibilityNodeInfo(child1.id)
+        val child2ANI = provider.createAccessibilityNodeInfo(child2.id)
+        val child2TraverseBefore = child2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val child1TraverseAfter = child1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALAFTER_VAL)
+
+        // We want child2 to come before child1
+        assertEquals(2, root.replacedChildren.size)
+        assertThat(child2TraverseBefore).isLessThan(child1.id)
+        assertThat(child1TraverseAfter).isLessThan(child2.id)
+    }
+
+    @Test
+    fun testChildrenSortedByBounds_rtl() {
+        val rootTag = "root"
+        val childTag1 = "child1"
+        val childTag2 = "child2"
+        val childTag3 = "child3"
+        val rtlChildTag1 = "rtlChild1"
+        val rtlChildTag2 = "rtlChild2"
+        val rtlChildTag3 = "rtlChild3"
+        container.setContent {
+            Column(Modifier.testTag(rootTag)) {
+                Row {
+                    SimpleTestLayout(
+                        Modifier
+                            .requiredSize(100.dp)
+                            .testTag(childTag1)
+                    ) {}
+                    SimpleTestLayout(
+                        Modifier
+                            .requiredSize(100.dp)
+                            .testTag(childTag2)
+                    ) {}
+                    SimpleTestLayout(
+                        Modifier
+                            .requiredSize(100.dp)
+                            .testTag(childTag3)
+                    ) {}
+                }
+                CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
+                    // Will display rtlChild3 rtlChild2 rtlChild1
+                    Row {
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(rtlChildTag1)
+                        ) {}
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(rtlChildTag2)
+                        ) {}
+                        SimpleTestLayout(
+                            Modifier
+                                .requiredSize(100.dp)
+                                .testTag(rtlChildTag3)
+                        ) {}
+                    }
+                }
+            }
+        }
+
+        val root = rule.onNodeWithTag(rootTag).fetchSemanticsNode()
+        assertEquals(6, root.replacedChildren.size)
+
+        val rtlChild1 = rule.onNodeWithTag(rtlChildTag1).fetchSemanticsNode()
+        val rtlChild2 = rule.onNodeWithTag(rtlChildTag2).fetchSemanticsNode()
+        val rtlChild3 = rule.onNodeWithTag(rtlChildTag3).fetchSemanticsNode()
+
+        val rtlChild1ANI = provider.createAccessibilityNodeInfo(rtlChild1.id)
+        val rtlChild2ANI = provider.createAccessibilityNodeInfo(rtlChild2.id)
+
+        val rtl1TraverseBefore = rtlChild1ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+        val rtl2TraverseBefore = rtlChild2ANI?.extras?.getInt(EXTRA_DATA_TEST_TRAVERSALBEFORE_VAL)
+
+        // Rtl
+        assertThat(rtl1TraverseBefore).isLessThan(rtlChild2.id)
+        assertThat(rtl2TraverseBefore).isLessThan(rtlChild3.id)
     }
 
     companion object {
@@ -3466,3 +4165,79 @@
             )
     }
 }
+
+/**
+ * A simple test layout that does the bare minimum required to lay out an arbitrary number of
+ * children reasonably.  Useful for Semantics hierarchy testing
+ */
+@Composable
+private fun SimpleTestLayout(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+    Layout(modifier = modifier, content = content) { measurables, constraints ->
+        if (measurables.isEmpty()) {
+            layout(constraints.minWidth, constraints.minHeight) {}
+        } else {
+            val placeables = measurables.map {
+                it.measure(constraints)
+            }
+            val (width, height) = with(placeables.filterNotNull()) {
+                Pair(
+                    max(
+                        maxByOrNull { it.width }?.width ?: 0,
+                        constraints.minWidth
+                    ),
+                    max(
+                        maxByOrNull { it.height }?.height ?: 0,
+                        constraints.minHeight
+                    )
+                )
+            }
+            layout(width, height) {
+                for (placeable in placeables) {
+                    placeable.placeRelative(0, 0)
+                }
+            }
+        }
+    }
+}
+
+/**
+ * A simple SubComposeLayout which lays [contentOne] at [positionOne] and lays [contentTwo] at
+ * [positionTwo]. [contentOne] is placed first and [contentTwo] is placed second. Therefore, the
+ * semantics node for [contentOne] is before semantics node for [contentTwo] in
+ * [SemanticsNode.children].
+ */
+@Composable
+private fun SimpleSubcomposeLayout(
+    modifier: Modifier = Modifier,
+    contentOne: @Composable () -> Unit,
+    positionOne: Offset,
+    contentTwo: @Composable () -> Unit,
+    positionTwo: Offset
+) {
+    SubcomposeLayout(modifier) { constraints ->
+        val layoutWidth = constraints.maxWidth
+        val layoutHeight = constraints.maxHeight
+
+        val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+
+        layout(layoutWidth, layoutHeight) {
+            val placeablesOne = subcompose(TestSlot.First, contentOne).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            val placeablesTwo = subcompose(TestSlot.Second, contentTwo).fastMap {
+                it.measure(looseConstraints)
+            }
+
+            // Placing to control drawing order to match default elevation of each placeable
+            placeablesOne.fastForEach {
+                it.place(positionOne.x.toInt(), positionOne.y.toInt())
+            }
+            placeablesTwo.fastForEach {
+                it.place(positionTwo.x.toInt(), positionTwo.y.toInt())
+            }
+        }
+    }
+}
+
+private enum class TestSlot { First, Second }
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 0e6b9f5..d7da687 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -19,16 +19,12 @@
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.requiredSize
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.text.BasicText
 import androidx.compose.material.Surface
 import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -38,8 +34,6 @@
 import androidx.compose.ui.layout.MeasurePolicy
 import androidx.compose.ui.layout.SubcomposeLayout
 import androidx.compose.ui.platform.InspectableValue
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.platform.ValueElement
 import androidx.compose.ui.platform.isDebugInspectorInfoEnabled
 import androidx.compose.ui.platform.testTag
@@ -56,8 +50,6 @@
 import androidx.compose.ui.test.onNodeWithContentDescription
 import androidx.compose.ui.test.onNodeWithTag
 import androidx.compose.ui.text.AnnotatedString
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.util.fastForEach
 import androidx.compose.ui.util.fastMap
@@ -839,472 +831,6 @@
             root.children[1].config.getOrNull(SemanticsProperties.TestTag)
         )
     }
-
-    @Test
-    fun testChildrenSortedByBounds_vertical_zIndex() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Column(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .zIndex(1f)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier.requiredSize(50.dp).semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_horizontal_zIndex() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Row(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .zIndex(1f)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier.requiredSize(50.dp).semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_vertical_offset() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .offset(x = 0.dp, y = 50.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier.requiredSize(50.dp).semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_horizontal_offset() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .offset(x = 50.dp, y = 0.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier.requiredSize(50.dp).semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_vertical_offset_overlapped() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .offset(x = 0.dp, y = 20.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier.requiredSize(50.dp).semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_horizontal_offset_overlapped() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .offset(x = 20.dp, y = 0.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(50.dp)
-                        .offset(x = 0.dp, y = 20.dp)
-                        .semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_vertical_offset_overlapped_withPadding() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(100.dp)
-                        .offset(x = 25.dp, y = 20.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(100.dp)
-                        .padding(25.dp)
-                        .semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_horizontal_offset_overlapped_withPadding() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(100.dp)
-                        .offset(x = 20.dp, y = 25.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(100.dp)
-                        .padding(25.dp)
-                        .semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_sameOffset_differentSize() {
-        val child1 = "child1"
-        val child2 = "child2"
-        rule.setContent {
-            Box(
-                Modifier.testTag(TestTag)
-            ) {
-                SimpleTestLayout(
-                    Modifier
-                        .requiredSize(100.dp)
-                        .offset(x = 50.dp, y = 0.dp)
-                        .semantics { testTag = child1 }
-                ) {}
-                SimpleTestLayout(
-                    Modifier.requiredSize(50.dp)
-                    .offset(x = 50.dp, y = 0.dp)
-                    .semantics { testTag = child2 }
-                ) {}
-            }
-        }
-
-        // Size should not be a factor.  z-order or placement order should break the tie instead.
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_vertical_subcompose() {
-        val child1 = "child1"
-        val child2 = "child2"
-        val density = Density(1f)
-        val size = with(density) { 100.dp.roundToPx() }.toFloat()
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides density) {
-                SimpleSubcomposeLayout(
-                    Modifier.testTag(TestTag),
-                    {
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = child1 }
-                        ) {}
-                    },
-                    Offset(0f, size),
-                    {
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = child2 }
-                        ) {}
-                    },
-                    Offset(0f, 0f)
-                )
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_horizontal_subcompose() {
-        val child1 = "child1"
-        val child2 = "child2"
-        val density = Density(1f)
-        val size = with(density) { 100.dp.roundToPx() }.toFloat()
-        rule.setContent {
-            CompositionLocalProvider(LocalDensity provides density) {
-                SimpleSubcomposeLayout(
-                    Modifier.testTag(TestTag),
-                    {
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = child1 }
-                        ) {}
-                    },
-                    Offset(size, 0f),
-                    {
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = child2 }
-                        ) {}
-                    },
-                    Offset(0f, 0f)
-                )
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(2, root.replacedChildrenSortedByBounds.size)
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
-
-    @Test
-    fun testChildrenSortedByBounds_rtl() {
-        val child1 = "child1"
-        val child2 = "child2"
-        val child3 = "child3"
-        val rtlChild1 = "rtlChild1"
-        val rtlChild2 = "rtlChild2"
-        val rtlChild3 = "rtlChild3"
-        rule.setContent {
-            Column(Modifier.testTag(TestTag)) {
-                Row {
-                    SimpleTestLayout(
-                        Modifier
-                            .requiredSize(100.dp)
-                            .semantics { testTag = child1 }
-                    ) {}
-                    SimpleTestLayout(
-                        Modifier
-                            .requiredSize(100.dp)
-                            .semantics { testTag = child2 }
-                    ) {}
-                    SimpleTestLayout(
-                        Modifier
-                            .requiredSize(100.dp)
-                            .semantics { testTag = child3 }
-                    ) {}
-                }
-                CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
-                    // Will display rtlChild3 rtlChild2 rtlChild1
-                    Row {
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = rtlChild1 }
-                        ) {}
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = rtlChild2 }
-                        ) {}
-                        SimpleTestLayout(
-                            Modifier
-                                .requiredSize(100.dp)
-                                .semantics { testTag = rtlChild3 }
-                        ) {}
-                    }
-                }
-            }
-        }
-
-        val root = rule.onNodeWithTag(TestTag).fetchSemanticsNode("can't find node $TestTag")
-        assertEquals(6, root.replacedChildrenSortedByBounds.size)
-
-        // Ltr
-        assertEquals(
-            child1,
-            root.replacedChildrenSortedByBounds[0].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child2,
-            root.replacedChildrenSortedByBounds[1].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            child3,
-            root.replacedChildrenSortedByBounds[2].config.getOrNull(SemanticsProperties.TestTag)
-        )
-
-        // Rtl
-        assertEquals(
-            rtlChild1,
-            root.replacedChildrenSortedByBounds[3].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            rtlChild2,
-            root.replacedChildrenSortedByBounds[4].config.getOrNull(SemanticsProperties.TestTag)
-        )
-        assertEquals(
-            rtlChild3,
-            root.replacedChildrenSortedByBounds[5].config.getOrNull(SemanticsProperties.TestTag)
-        )
-    }
 }
 
 private fun SemanticsNodeInteraction.assertDoesNotHaveProperty(property: SemanticsPropertyKey<*>) {
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index c9003f7..d0c2a7d 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -103,6 +103,74 @@
 import kotlin.math.floor
 import kotlin.math.roundToInt
 import kotlin.math.sign
+import kotlin.math.max
+import kotlin.math.min
+
+// TODO(mnuzen): This code is copy-pasted from experimental API in the Kotlin 1.7 standard library: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/range-until.html.
+// Delete it when this API graduates to stable in Kotlin (when the docs page linked no longer has @ExperimentalStdlibApi annotation).
+/**
+ * Represents a range of values (for example, numbers or characters) where the upper bound is not included in the range.
+ * See the [Kotlin language documentation](https://kotlinlang.org/docs/reference/ranges.html) for more information.
+ */
+internal interface OpenEndRange> {
+    /**
+     * The minimum value in the range.
+     */
+    val start: T
+
+    /**
+     * The maximum value in the range (exclusive).
+     *
+     * @throws IllegalStateException can be thrown if the exclusive end bound cannot be represented
+     * with a value of type [T].
+     */
+    val endExclusive: T
+
+    /**
+     * Checks whether the specified [value] belongs to the range.
+     *
+     * A value belongs to the open-ended range if it is greater than or equal to the [start] bound and strictly less than the [endExclusive] bound.
+     */
+    operator fun contains(value: T): Boolean = value >= start && value < endExclusive
+
+    /**
+     * Checks whether the range is empty.
+     *
+     * The open-ended range is empty if its start value is greater than or equal to the end value.
+     */
+    fun isEmpty(): Boolean = start >= endExclusive
+}
+
+private class OpenEndFloatRange(
+    start: Float,
+    endExclusive: Float
+) : OpenEndRange {
+    private val _start = start
+    private val _endExclusive = endExclusive
+    override val start: Float get() = _start
+    override val endExclusive: Float get() = _endExclusive
+
+    private fun lessThanOrEquals(a: Float, b: Float): Boolean = a <= b
+
+    override fun contains(value: Float): Boolean = value >= _start && value < _endExclusive
+    override fun isEmpty(): Boolean = !(_start < _endExclusive)
+
+    override fun equals(other: Any?): Boolean {
+        return other is OpenEndFloatRange && (isEmpty() && other.isEmpty() ||
+            _start == other._start && _endExclusive == other._endExclusive)
+    }
+
+    override fun hashCode(): Int {
+        return if (isEmpty()) -1 else 31 * _start.hashCode() + _endExclusive.hashCode()
+    }
+
+    override fun toString(): String = "$_start..<$_endExclusive"
+}
+internal operator fun Float.rangeUntil(that: Float): OpenEndRange =
+    OpenEndFloatRange(this, that)
+
+private fun OpenEndRange.overlaps(it: OpenEndRange) =
+    !isEmpty() && !it.isEmpty() && max(start, it.start) < min(endExclusive, it.endExclusive)
 
 private fun LayoutNode.findClosestParentNode(selector: (LayoutNode) -> Boolean): LayoutNode? {
     var currentParent = this.parent
@@ -444,45 +512,173 @@
         return info.unwrap()
     }
 
+    private fun semanticComparator(
+        layoutIsRtl: Boolean,
+    ): Comparator {
+        // First compare the coordinates LTR
+        var comparator = compareBy (
+            { it.layoutNode.coordinates.boundsInWindow().left },
+            { it.layoutNode.coordinates.boundsInWindow().top },
+            { it.layoutNode.coordinates.boundsInWindow().bottom },
+            { it.layoutNode.coordinates.boundsInWindow().right })
+        // Modify comparison if we're not using LTR comparison strategy to use RTL instead
+        if (layoutIsRtl) {
+            comparator = compareBy(
+                { it.layoutNode.coordinates.boundsInWindow().right },
+                { it.layoutNode.coordinates.boundsInWindow().top },
+                { it.layoutNode.coordinates.boundsInWindow().bottom },
+                { it.layoutNode.coordinates.boundsInWindow().left })
+        }
+        return comparator
+            // then compare by layoutNode's zIndex and placement order
+            .thenBy(LayoutNode.ZComparator) { it.layoutNode }
+            // then compare by semanticsId to break the tie somehow
+            .thenBy { it.id }
+    }
+
+    /**
+     * Returns the results of geometry groupings, which is determined from 1) grouping nodes into
+     * distinct, non-overlapping rows based on their top/bottom coordinates, then 2) sorting nodes
+     * within each row with the semantics comparator.
+     *
+     * This method approaches traversal order with more nuance than an approach considering only
+     * just hierarchy or only just an individual node's bounds.
+     *
+     * If [containerChildrenMapping] exists, there are additional children to add, as well as the
+     * sorted parent itself
+     */
+    private fun sortByGeometryGroupings(
+        layoutIsRtl: Boolean,
+        parentListToSort: MutableList,
+        containerChildrenMapping: MutableMap> = mutableMapOf()
+    ): MutableList {
+        // RowGroupings list consists of pairs, first = a rectangle of the bounds of the row
+        // and second = the list of nodes in that row
+        val rowGroupings = mutableListOf>>()
+
+        // check to see if this entry overlaps with any groupings in rowGroupings
+        fun placedEntryRowOverlaps(
+            node: SemanticsNode
+        ): Boolean {
+            // Conversion to long is needed in order to utilize `until`, which has no float ver
+            val entryTopCoord = node.layoutNode.coordinates.boundsInWindow().top
+            val entryBottomCoord = node.layoutNode.coordinates.boundsInWindow().bottom
+            val entryRange = entryTopCoord.rangeUntil(entryBottomCoord)
+
+            for (currIndex in 0..rowGroupings.lastIndex) {
+                var currRect = rowGroupings[currIndex].first
+                var groupRange = currRect.top.rangeUntil(currRect.bottom)
+
+                // If it overlaps with this row group, update cover and add node
+                if (groupRange.overlaps(entryRange)) {
+                    val newRect = currRect.intersect(
+                        Rect(
+                            0f,
+                            entryTopCoord,
+                            Float.POSITIVE_INFINITY,
+                            entryBottomCoord
+                        )
+                    )
+                    // Replace the cover rectangle, copying over the old list of nodes
+                    rowGroupings[currIndex] = Pair(newRect, rowGroupings[currIndex].second)
+                    // Add current node
+                    rowGroupings[currIndex].second.add(node)
+                    // We've found an overlapping group, return true
+                    return true
+                }
+            }
+
+            // If we've made it here, then there are no groups our entry overlaps with
+            return false
+        }
+
+        for (entryIndex in 0..parentListToSort.lastIndex) {
+            val currEntry = parentListToSort[entryIndex]
+            // If this is the first entry, or vertical groups don't overlap
+            if (entryIndex == 0 || !placedEntryRowOverlaps(currEntry)) {
+                val newRect = currEntry.layoutNode.coordinates.boundsInWindow()
+                rowGroupings.add(Pair(newRect, mutableListOf(currEntry)))
+            } // otherwise, we've already iterated through, found and placed it in a matching group
+        }
+
+        // Sort the rows from top to bottom
+        rowGroupings.sortWith(compareBy({ it.first.top }, { it.first.bottom }))
+
+        val returnList = mutableListOf()
+        rowGroupings.fastForEach { row ->
+            // Sort each individual row's parent nodes
+            row.second.sortWith(semanticComparator(layoutIsRtl))
+            row.second.fastForEach { node ->
+                // If a parent node is a container, then add its children
+                // Otherwise, simply add the parent node
+                returnList.addAll(containerChildrenMapping[node.id] ?: mutableListOf(node))
+            }
+        }
+
+        return returnList
+    }
+
+    /**
+     * This function prepares a subtree for `sortByGeometryGroupings` by retrieving all
+     * non-container nodes and adding them to the list to be geometrically sorted. We recurse on
+     * containers (if they exist) and add their sorted children to an optional mapping.
+     * The list to be sorted and child mapping is passed into `sortByGeometryGroupings`.
+     */
+    private fun subtreeSortedByGeometryGrouping(
+        layoutIsRtl: Boolean,
+        listToSort: MutableList
+    ): MutableList {
+        // This should be mapping of [containerID: listOfSortedChildren], only populated if there
+        // are container nodes in this level. If there are container nodes, `containerMapToChildren`
+        // would look like {containerId: [sortedChild, sortedChild], containerId: [sortedChild]}
+        val containerMapToChildren = mutableMapOf>()
+        val geometryList = mutableListOf()
+
+        fun depthFirstSearch(currNode: SemanticsNode) {
+            // Add this node to the list we will eventually sort
+            geometryList.add(currNode)
+            if (currNode.semanticsNodeIsStructurallySignificant) {
+                // Recurse and record the container's children, sorted
+                containerMapToChildren[currNode.id] = subtreeSortedByGeometryGrouping(
+                    layoutIsRtl, currNode.children.toMutableList()
+                )
+            } else {
+                // Otherwise, continue adding children to the list that'll be sorted regardless of
+                // hierarchy
+                currNode.children.fastForEach { child ->
+                    depthFirstSearch(child)
+                }
+            }
+        }
+
+        listToSort.fastForEach { node ->
+            depthFirstSearch(node)
+        }
+
+        return sortByGeometryGroupings(layoutIsRtl, geometryList, containerMapToChildren)
+    }
+
     private fun setTraversalValues() {
         idToBeforeMap.clear()
         idToAfterMap.clear()
-        var idToCoordinatesList = mutableListOf>()
 
-        fun depthFirstSearch(currNode: SemanticsNode) {
-            if (currNode.parent?.layoutNode?.innerCoordinator?.isAttached == true &&
-                currNode.layoutNode.innerCoordinator.isAttached
-            ) {
-                idToCoordinatesList.add(
-                    Pair(
-                        currNode.id,
-                        currNode.layoutNode.coordinates.boundsInWindow()
-                    )
-                )
-            }
-            // This retrieves the children in the order that we want (respecting child/parent
-            // hierarchies)
-            currNode.replacedChildrenSortedByBounds.fastForEach { child ->
-                depthFirstSearch(child)
-            }
-        }
+        val hostSemanticsNode =
+            currentSemanticsNodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]
+                ?.semanticsNode!!
+        val layoutIsRtl = hostSemanticsNode.isRtl
 
-        currentSemanticsNodes[AccessibilityNodeProviderCompat.HOST_VIEW_ID]?.semanticsNode?.let {
-            depthFirstSearch(
-                it
-            )
-        }
+        val semanticsOrderList = subtreeSortedByGeometryGrouping(
+            layoutIsRtl, hostSemanticsNode.children.toMutableList()
+        )
 
         // Iterate through our ordered list, and creating a mapping of current node to next node ID
         // We'll later read through this and set traversal order with IdToBeforeMap
-        for (i in 1..idToCoordinatesList.lastIndex) {
-            val prevId = idToCoordinatesList[i - 1].first
-            val currId = idToCoordinatesList[i].first
+        for (i in 1..semanticsOrderList.lastIndex) {
+            val prevId = semanticsOrderList[i - 1].id
+            val currId = semanticsOrderList[i].id
             idToBeforeMap[prevId] = currId
             idToAfterMap[currId] = prevId
         }
-
-        return
     }
 
     @VisibleForTesting
@@ -2746,6 +2942,23 @@
 private val SemanticsNode.isPassword: Boolean get() = config.contains(SemanticsProperties.Password)
 private val SemanticsNode.isTextField get() = this.unmergedConfig.contains(SemanticsActions.SetText)
 private val SemanticsNode.isRtl get() = layoutInfo.layoutDirection == LayoutDirection.Rtl
+private val SemanticsNode.isContainer get() = config.getOrNull(SemanticsProperties.IsContainer)
+private val SemanticsNode.hasCollectionInfo get() =
+    config.contains(SemanticsProperties.CollectionInfo)
+private val SemanticsNode.isScrollable get() = config.contains(SemanticsActions.ScrollBy)
+
+private val SemanticsNode.semanticsNodeIsStructurallySignificant: Boolean
+    get() {
+        // We check if `isContainer == false` first to ensure if this flag is set, the node is not
+        // considered structural, even if it is a collection or a scrollable.
+        if (this.isContainer == false) {
+            return false
+        } else if (this.isContainer == true ||
+            this.hasCollectionInfo || this.isScrollable) {
+            return true
+        }
+        return false
+    }
 
 @OptIn(ExperimentalComposeUiApi::class)
 private fun SemanticsNode.excludeLineAndPageGranularities(): Boolean {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 8b3d63d..0bf327d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -201,18 +201,14 @@
         get() = mergingEnabled && unmergedConfig.isMergingSemanticsOfDescendants
 
     internal fun unmergedChildren(
-        sortByBounds: Boolean = false,
         includeFakeNodes: Boolean = false
     ): List {
         // TODO(lmr): we should be able to do this more efficiently using visitSubtree
         if (this.isFake) return listOf()
         val unmergedChildren: MutableList = mutableListOf()
 
-        val semanticsChildren = if (sortByBounds) {
-            this.layoutNode.findOneLayerOfSemanticsWrappersSortedByBounds()
-        } else {
-            this.layoutNode.findOneLayerOfSemanticsWrappers()
-        }
+        val semanticsChildren = this.layoutNode.findOneLayerOfSemanticsWrappers()
+
         semanticsChildren.fastForEach { semanticsChild ->
             unmergedChildren.add(SemanticsNode(semanticsChild, mergingEnabled))
         }
@@ -234,7 +230,6 @@
     //               optimize this when the merging algorithm is improved.
     val children: List
         get() = getChildren(
-            sortByBounds = false,
             includeReplacedSemantics = !mergingEnabled,
             includeFakeNodes = false
         )
@@ -248,27 +243,11 @@
      */
     internal val replacedChildren: List
         get() = getChildren(
-            sortByBounds = false,
             includeReplacedSemantics = false,
             includeFakeNodes = true
         )
 
     /**
-     * Similar to [replacedChildren] but children are sorted by bounds: top to down, left to
-     * right(right to left in RTL mode).
-     */
-    // TODO(b/184376083): This is too expensive for a val (full subtree recreation every call);
-    //               optimize this when the merging algorithm is improved.
-    internal val replacedChildrenSortedByBounds: List
-        get() = getChildren(
-            sortByBounds = true,
-            includeReplacedSemantics = false,
-            includeFakeNodes = true
-        )
-
-    /**
-     * @param sortByBounds if true, nodes in the result list will be sorted with respect to their
-     * bounds. Otherwise children will be in the order they are added to the composition
      * @param includeReplacedSemantics if true, the result will contain children of nodes marked
      * as [clearAndSetSemantics]. For accessibility we always use false, but in testing and
      * debugging we should be able to investigate both
@@ -278,7 +257,6 @@
      * and so will be this parameter.
      */
     private fun getChildren(
-        sortByBounds: Boolean,
         includeReplacedSemantics: Boolean,
         includeFakeNodes: Boolean
     ): List {
@@ -290,10 +268,10 @@
             // In most common merging scenarios like Buttons, this will return nothing.
             // In cases like a clickable Row itself containing a Button, this will
             // return the Button as a child.
-            return findOneLayerOfMergingSemanticsNodes(sortByBounds = sortByBounds)
+            return findOneLayerOfMergingSemanticsNodes()
         }
 
-        return unmergedChildren(sortByBounds, includeFakeNodes)
+        return unmergedChildren(includeFakeNodes)
     }
 
     /**
@@ -327,10 +305,9 @@
         }
 
     private fun findOneLayerOfMergingSemanticsNodes(
-        list: MutableList = mutableListOf(),
-        sortByBounds: Boolean = false
+        list: MutableList = mutableListOf()
     ): List {
-        unmergedChildren(sortByBounds).fastForEach { child ->
+        unmergedChildren().fastForEach { child ->
             if (child.isMergingSemanticsOfDescendants) {
                 list.add(child)
             } else {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt
index 9fd19fe..62a5bdb 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsSort.kt
@@ -1,5 +1,6 @@
+@file:Suppress("ktlint")
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright 2023 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,178 +17,8 @@
 
 package androidx.compose.ui.semantics
 
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.layout.boundsInRoot
 import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.NodeCoordinator
-import androidx.compose.ui.node.SemanticsModifierNode
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.util.fastForEach
-import androidx.compose.ui.util.fastMap
 
-// This part is a copy from ViewGroup#addChildrenForAccessibility.
-@OptIn(ExperimentalComposeUiApi::class)
-internal fun LayoutNode.findOneLayerOfSemanticsWrappersSortedByBounds(
-    list: MutableList = mutableListOf()
-): List {
-    fun sortWithStrategy(holders: List): List {
-        // This is gross but the least risky solution. The current comparison
-        // strategy breaks transitivity but produces very good results. Coming
-        // up with a new strategy requires time which we do not have, so ...
-        return try {
-            NodeLocationHolder.comparisonStrategy = NodeLocationHolder.ComparisonStrategy.Stripe
-            holders.toMutableList().apply { sort() }
-        } catch (iae: IllegalArgumentException) {
-            // Note that in practice this occurs extremely rarely in a couple
-            // of pathological cases.
-            NodeLocationHolder.comparisonStrategy = NodeLocationHolder.ComparisonStrategy.Location
-            holders.toMutableList().apply { sort() }
-        }
-    }
-
-    if (!isAttached) {
-        return list
-    }
-    val holders = ArrayList()
-    children.fastForEach {
-        if (it.isAttached) holders.add(NodeLocationHolder(this, it))
-    }
-    val sortedChildren = sortWithStrategy(holders).fastMap { it.node }
-
-    sortedChildren.fastForEach { child ->
-        val outerSemantics = child.outerSemantics
-        if (outerSemantics != null) {
-            list.add(outerSemantics)
-        } else {
-            child.findOneLayerOfSemanticsWrappersSortedByBounds(list)
-        }
-    }
-    return list
-}
-
-internal class NodeLocationHolder internal constructor(
-    internal val subtreeRoot: LayoutNode,
-    internal val node: LayoutNode
-) : Comparable {
-    internal companion object {
-        internal var comparisonStrategy = ComparisonStrategy.Stripe
-    }
-
-    internal enum class ComparisonStrategy { Stripe, Location }
-
-    private val location: Rect?
-
-    private val layoutDirection = subtreeRoot.layoutDirection
-
-    init {
-        val subtreeRootCoordinator = subtreeRoot.innerCoordinator
-        val coordinator = node.findCoordinatorToGetBounds()
-        location = if (subtreeRootCoordinator.isAttached && coordinator.isAttached) {
-            subtreeRootCoordinator.localBoundingBoxOf(coordinator)
-        } else {
-            null
-        }
-    }
-
-    override fun compareTo(other: NodeLocationHolder): Int {
-        if (location == null) {
-            // put the unattached nodes at last. This probably can save accessibility services time.
-            return 1
-        }
-        if (other.location == null) {
-            return -1
-        }
-
-        if (comparisonStrategy == ComparisonStrategy.Stripe) {
-            // First is above second.
-            if (location.bottom - other.location.top <= 0) {
-                return -1
-            }
-            // First is below second.
-            if (location.top - other.location.bottom >= 0) {
-                return 1
-            }
-        }
-
-        // We are ordering left-to-right, top-to-bottom.
-        if (layoutDirection == LayoutDirection.Ltr) {
-            val leftDifference = location.left - other.location.left
-            if (leftDifference != 0f) {
-                return if (leftDifference < 0) -1 else 1
-            }
-        } else { // RTL
-            val rightDifference = location.right - other.location.right
-            if (rightDifference != 0f) {
-                return if (rightDifference < 0) 1 else -1
-            }
-        }
-        // We are ordering left-to-right, top-to-bottom.
-        val topDifference = location.top - other.location.top
-        if (topDifference != 0f) {
-            return if (topDifference < 0) -1 else 1
-        }
-
-        // Find a child of each view with different screen bounds. If we get here, node and
-        // other.node must be attached.
-        val view1Bounds = node.findCoordinatorToGetBounds().boundsInRoot()
-        val view2Bounds = other.node.findCoordinatorToGetBounds().boundsInRoot()
-        val child1 = node.findNodeByPredicateTraversal {
-            val wrapper = it.findCoordinatorToGetBounds()
-            wrapper.isAttached && view1Bounds != wrapper.boundsInRoot()
-        }
-        val child2 = other.node.findNodeByPredicateTraversal {
-            val wrapper = it.findCoordinatorToGetBounds()
-            wrapper.isAttached && view2Bounds != wrapper.boundsInRoot()
-        }
-        // Compare the children recursively
-        if ((child1 != null) && (child2 != null)) {
-            val childHolder1 = NodeLocationHolder(subtreeRoot, child1)
-            val childHolder2 = NodeLocationHolder(other.subtreeRoot, child2)
-            return childHolder1.compareTo(childHolder2)
-        }
-
-        // If only one has a child, use that one
-        if (child1 != null) {
-            return 1
-        }
-
-        if (child2 != null) {
-            return -1
-        }
-
-        val zDifference = LayoutNode.ZComparator.compare(node, other.node)
-        if (zDifference != 0) {
-            return -zDifference
-        }
-
-        // Break tie somehow
-        return node.semanticsId - other.node.semanticsId
-    }
-}
-
-internal fun LayoutNode.findNodeByPredicateTraversal(
-    predicate: (LayoutNode) -> Boolean
-): LayoutNode? {
-    if (predicate(this)) {
-        return this
-    }
-
-    children.fastForEach {
-        val result = it.findNodeByPredicateTraversal(predicate)
-        if (result != null) {
-            return result
-        }
-    }
-
-    return null
-}
-
-/**
- * If this node has semantics, we use the semantics wrapper to get bounds. Otherwise, we use
- * innerCoordinator because it seems the bounds after padding is the effective content.
- */
-@OptIn(ExperimentalComposeUiApi::class)
-internal fun LayoutNode.findCoordinatorToGetBounds(): NodeCoordinator {
-    return (outerMergingSemantics ?: outerSemantics)?.node?.coordinator ?: innerCoordinator
-}
+internal fun LayoutNode.findOneLayerOfSemanticsWrappersSortedByBounds() {
+    fun shimToPreserveFilenameAPI() {}
+}
\ No newline at end of file
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/semantics/SemanticsSortTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/semantics/SemanticsSortTest.kt
deleted file mode 100644
index 47ac9fe4..0000000
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/semantics/SemanticsSortTest.kt
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.compose.ui.semantics
-
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.node.LayoutNode
-import androidx.compose.ui.node.MockOwner
-import androidx.compose.ui.node.SemanticsModifierNode
-import androidx.compose.ui.zIndex
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@OptIn(ExperimentalComposeUiApi::class)
-@RunWith(JUnit4::class)
-class SemanticsSortTest {
-
-    @Test // regression test for b/207477257
-    fun compareDoesNotViolateComparatorContract() {
-        val root = LayoutNode(0, 0, 720, 1080)
-        repeat(32) { index ->
-            val child = if (index % 2 == 0) {
-                LayoutNode(0, 0, 0, 0)
-            } else {
-                val offset = if (index == 1 || index == 31) 100 else 0
-                LayoutNode(0, 0 - offset, 720, 30 - offset).also {
-                    it.insertAt(0, LayoutNode(0, 0, 100, 100, Modifier.semantics { }))
-                }
-            }
-            root.insertAt(index, child)
-        }
-
-        root.attach(MockOwner())
-        root.findOneLayerOfSemanticsWrappersSortedByBounds()
-
-        // expect - no crash happened
-    }
-
-    @Test
-    fun sortedByZOrderIfHasSameBounds() {
-        val root = LayoutNode(0, 0, 100, 100)
-        repeat(5) { index ->
-            root.insertAt(
-                index,
-                LayoutNode(
-                    0, 0, 100, 100,
-                    Modifier
-                        .semantics { set(LayoutNodeIndex, index) }
-                        .zIndex((index * 3 % 5).toFloat())
-                )
-            )
-        }
-        root.attach(MockOwner())
-        root.remeasure()
-        root.replace()
-        val result = root.findOneLayerOfSemanticsWrappersSortedByBounds()
-
-        assertThat(result[0].layoutNodeIndex()).isEqualTo(3)
-        assertThat(result[1].layoutNodeIndex()).isEqualTo(1)
-        assertThat(result[2].layoutNodeIndex()).isEqualTo(4)
-        assertThat(result[3].layoutNodeIndex()).isEqualTo(2)
-        assertThat(result[4].layoutNodeIndex()).isEqualTo(0)
-    }
-
-    private val LayoutNodeIndex = SemanticsPropertyKey("LayoutNodeIndex")
-
-    private fun SemanticsModifierNode.layoutNodeIndex(): Int {
-        return semanticsConfiguration[LayoutNodeIndex]
-    }
-}