Moved NestedScrollConnection into SwipeToScene

This change allows us to consume all nested scroll gestures of the
scrollable elements in the scene without having to specify an additional
 modifier above the scrollable components.
The SceneTransitionLayout now has a default nested scroll behavior that
can be changed via the previous nestedScrollToScene() modifier.

Test: atest NestedScrollToSceneTest
Test: atest ElementTest (nestedScrollToScene is not mandatory anymore)
Test: Manually tested on Flexiglass
Bug: 336710600
Flag: com.android.systemui.scene_container
Change-Id: Icade65f33b84a9784aef599a62d0485dc65088db
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index 733affd..3f60e9b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -911,9 +911,9 @@
 internal class NestedScrollHandlerImpl(
     private val layoutImpl: SceneTransitionLayoutImpl,
     private val orientation: Orientation,
-    private val topOrLeftBehavior: NestedScrollBehavior,
-    private val bottomOrRightBehavior: NestedScrollBehavior,
-    private val isExternalOverscrollGesture: () -> Boolean,
+    internal var topOrLeftBehavior: NestedScrollBehavior,
+    internal var bottomOrRightBehavior: NestedScrollBehavior,
+    internal var isExternalOverscrollGesture: () -> Boolean,
     private val pointersInfoOwner: PointersInfoOwner,
 ) {
     private val layoutState = layoutImpl.state
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
index 615d393..2b78b5a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt
@@ -41,7 +41,6 @@
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.node.ObserverModifierNode
 import androidx.compose.ui.node.PointerInputModifierNode
-import androidx.compose.ui.node.TraversableNode
 import androidx.compose.ui.node.currentValueOf
 import androidx.compose.ui.node.findNearestAncestor
 import androidx.compose.ui.node.observeReads
@@ -139,16 +138,12 @@
     DelegatingNode(),
     PointerInputModifierNode,
     CompositionLocalConsumerModifierNode,
-    TraversableNode,
-    PointersInfoOwner,
     ObserverModifierNode {
     private val pointerInputHandler: suspend PointerInputScope.() -> Unit = { pointerInput() }
     private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler))
     private val velocityTracker = VelocityTracker()
     private var previousEnabled: Boolean = false
 
-    override val traverseKey: Any = TRAVERSE_KEY
-
     var enabled: () -> Boolean = enabled
         set(value) {
             // Reset the pointer input whenever enabled changed.
@@ -208,7 +203,7 @@
     private var startedPosition: Offset? = null
     private var pointersDown: Int = 0
 
-    override fun pointersInfo(): PointersInfo {
+    internal fun pointersInfo(): PointersInfo {
         return PointersInfo(
             startedPosition = startedPosition,
             // Note: We could have 0 pointers during fling or for other reasons.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
index 1e36f3d..945043d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt
@@ -22,11 +22,9 @@
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
 import androidx.compose.ui.input.nestedscroll.NestedScrollSource
 import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
-import androidx.compose.ui.node.DelegatableNode
 import androidx.compose.ui.node.DelegatingNode
 import androidx.compose.ui.node.ModifierNodeElement
 import androidx.compose.ui.platform.InspectorInfo
-import com.android.compose.nestedscroll.PriorityNestedScrollConnection
 
 /**
  * Defines the behavior of the [SceneTransitionLayout] when a scrollable component is scrolled.
@@ -70,7 +68,11 @@
      * In addition, during scene transitions, scroll events are consumed by the
      * [SceneTransitionLayout] instead of the scrollable component.
      */
-    EdgeAlways(canStartOnPostFling = true),
+    EdgeAlways(canStartOnPostFling = true);
+
+    companion object {
+        val Default = EdgeNoPreview
+    }
 }
 
 internal fun Modifier.nestedScrollToScene(
@@ -131,7 +133,6 @@
     private var bottomOrRightBehavior: NestedScrollBehavior,
     private var isExternalOverscrollGesture: () -> Boolean,
 ) : DelegatingNode() {
-    lateinit var pointersInfoOwner: PointersInfoOwner
     private var scrollBehaviorOwner: ScrollBehaviorOwner? = null
 
     private fun requireScrollBehaviorOwner(): ScrollBehaviorOwner {
@@ -143,7 +144,7 @@
         return behaviorOwner
     }
 
-    val updateScrollBehaviorsConnection =
+    private val updateScrollBehaviorsConnection =
         object : NestedScrollConnection {
             /**
              * When using [NestedScrollConnection.onPostScroll], we can specify the desired behavior
@@ -174,37 +175,11 @@
             }
         }
 
-    private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
-        scenePriorityNestedScrollConnection(
-            layoutImpl = layoutImpl,
-            orientation = orientation,
-            topOrLeftBehavior = topOrLeftBehavior,
-            bottomOrRightBehavior = bottomOrRightBehavior,
-            isExternalOverscrollGesture = isExternalOverscrollGesture,
-            pointersInfoOwner = { pointersInfoOwner.pointersInfo() }
-        )
-
-    private var nestedScrollNode: DelegatableNode =
-        nestedScrollModifierNode(
-            connection = priorityNestedScrollConnection,
-            dispatcher = null,
-        )
-
-    private var updateScrollBehaviorsNestedScrollNode: DelegatableNode =
-        nestedScrollModifierNode(
-            connection = updateScrollBehaviorsConnection,
-            dispatcher = null,
-        )
-
-    override fun onAttach() {
-        pointersInfoOwner = requireAncestorPointersInfoOwner()
-        delegate(nestedScrollNode)
-        delegate(updateScrollBehaviorsNestedScrollNode)
+    init {
+        delegate(nestedScrollModifierNode(updateScrollBehaviorsConnection, dispatcher = null))
     }
 
     override fun onDetach() {
-        // Make sure we reset the scroll connection when this modifier is removed from composition
-        priorityNestedScrollConnection.reset()
         scrollBehaviorOwner = null
     }
 
@@ -220,44 +195,5 @@
         this.topOrLeftBehavior = topOrLeftBehavior
         this.bottomOrRightBehavior = bottomOrRightBehavior
         this.isExternalOverscrollGesture = isExternalOverscrollGesture
-
-        // Clean up the old nested scroll connection
-        priorityNestedScrollConnection.reset()
-        undelegate(nestedScrollNode)
-
-        // Create a new nested scroll connection
-        priorityNestedScrollConnection =
-            scenePriorityNestedScrollConnection(
-                layoutImpl = layoutImpl,
-                orientation = orientation,
-                topOrLeftBehavior = topOrLeftBehavior,
-                bottomOrRightBehavior = bottomOrRightBehavior,
-                isExternalOverscrollGesture = isExternalOverscrollGesture,
-                pointersInfoOwner = pointersInfoOwner,
-            )
-        nestedScrollNode =
-            nestedScrollModifierNode(
-                connection = priorityNestedScrollConnection,
-                dispatcher = null,
-            )
-        delegate(nestedScrollNode)
     }
 }
-
-private fun scenePriorityNestedScrollConnection(
-    layoutImpl: SceneTransitionLayoutImpl,
-    orientation: Orientation,
-    topOrLeftBehavior: NestedScrollBehavior,
-    bottomOrRightBehavior: NestedScrollBehavior,
-    isExternalOverscrollGesture: () -> Boolean,
-    pointersInfoOwner: PointersInfoOwner,
-) =
-    NestedScrollHandlerImpl(
-            layoutImpl = layoutImpl,
-            orientation = orientation,
-            topOrLeftBehavior = topOrLeftBehavior,
-            bottomOrRightBehavior = bottomOrRightBehavior,
-            isExternalOverscrollGesture = isExternalOverscrollGesture,
-            pointersInfoOwner = pointersInfoOwner,
-        )
-        .connection
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index 45758c5..4b94013 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -204,8 +204,8 @@
      * @param rightBehavior when we should perform the overscroll animation at the right.
      */
     fun Modifier.horizontalNestedScrollToScene(
-        leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
-        rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+        leftBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
+        rightBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
         isExternalOverscrollGesture: () -> Boolean = { false },
     ): Modifier
 
@@ -217,8 +217,8 @@
      * @param bottomBehavior when we should perform the overscroll animation at the bottom.
      */
     fun Modifier.verticalNestedScrollToScene(
-        topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
-        bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+        topBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
+        bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.Default,
         isExternalOverscrollGesture: () -> Boolean = { false },
     ): Modifier
 
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
index c68f2ec..36a2f14 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt
@@ -20,6 +20,7 @@
 import androidx.compose.runtime.Stable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode
 import androidx.compose.ui.input.pointer.PointerEvent
 import androidx.compose.ui.input.pointer.PointerEventPass
 import androidx.compose.ui.node.DelegatableNode
@@ -81,8 +82,24 @@
             }
         }
 
-    override fun onAttach() {
-        delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey))
+    private val nestedScrollHandlerImpl =
+        NestedScrollHandlerImpl(
+            layoutImpl = draggableHandler.layoutImpl,
+            orientation = draggableHandler.orientation,
+            topOrLeftBehavior = NestedScrollBehavior.Default,
+            bottomOrRightBehavior = NestedScrollBehavior.Default,
+            isExternalOverscrollGesture = { false },
+            pointersInfoOwner = { multiPointerDraggableNode.pointersInfo() },
+        )
+
+    init {
+        delegate(nestedScrollModifierNode(nestedScrollHandlerImpl.connection, dispatcher = null))
+        delegate(ScrollBehaviorOwnerNode(draggableHandler.nestedScrollKey, nestedScrollHandlerImpl))
+    }
+
+    override fun onDetach() {
+        // Make sure we reset the scroll connection when this modifier is removed from composition
+        nestedScrollHandlerImpl.connection.reset()
     }
 
     override fun onPointerEvent(
@@ -149,13 +166,17 @@
  *
  * TODO(b/353234530) move this logic into [SwipeToSceneNode]
  */
-private class ScrollBehaviorOwnerNode(override val traverseKey: Any) :
-    Modifier.Node(), TraversableNode, ScrollBehaviorOwner {
+private class ScrollBehaviorOwnerNode(
+    override val traverseKey: Any,
+    val nestedScrollHandlerImpl: NestedScrollHandlerImpl
+) : Modifier.Node(), TraversableNode, ScrollBehaviorOwner {
     override fun updateScrollBehaviors(
         topOrLeftBehavior: NestedScrollBehavior,
         bottomOrRightBehavior: NestedScrollBehavior,
         isExternalOverscrollGesture: () -> Boolean
     ) {
-        // This method will be used to update the desired behavior in a following CL.
+        nestedScrollHandlerImpl.topOrLeftBehavior = topOrLeftBehavior
+        nestedScrollHandlerImpl.bottomOrRightBehavior = bottomOrRightBehavior
+        nestedScrollHandlerImpl.isExternalOverscrollGesture = isExternalOverscrollGesture
     }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 7988e0e..c91151e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -797,8 +797,6 @@
                 scene(SceneB, userActions = mapOf(Swipe.Up to SceneA)) {
                     Box(
                         Modifier
-                            // Unconsumed scroll gesture will be intercepted by STL
-                            .verticalNestedScrollToScene()
                             // A scrollable that does not consume the scroll gesture
                             .scrollable(
                                 rememberScrollableState(consumeScrollDelta = { 0f }),
@@ -875,8 +873,6 @@
                 ) {
                     Box(
                         Modifier
-                            // Unconsumed scroll gesture will be intercepted by STL
-                            .verticalNestedScrollToScene()
                             // A scrollable that does not consume the scroll gesture
                             .scrollable(
                                 rememberScrollableState(consumeScrollDelta = { 0f }),
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
new file mode 100644
index 0000000..311a580
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/NestedScrollToSceneTest.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 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.
+ * 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 com.android.compose.animation.scene
+
+import androidx.compose.foundation.gestures.Orientation.Vertical
+import androidx.compose.foundation.gestures.rememberScrollableState
+import androidx.compose.foundation.gestures.scrollable
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalViewConfiguration
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.TestScenes.SceneB
+import com.android.compose.animation.scene.subjects.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NestedScrollToSceneTest {
+    @get:Rule val rule = createComposeRule()
+
+    private var touchSlop = 0f
+    private val layoutWidth: Dp = 200.dp
+    private val layoutHeight = 400.dp
+
+    private fun setup2ScenesAndScrollTouchSlop(
+        modifierSceneA: @Composable SceneScope.() -> Modifier = { Modifier },
+    ): MutableSceneTransitionLayoutState {
+        val state =
+            rule.runOnUiThread {
+                MutableSceneTransitionLayoutState(SceneA, transitions = EmptyTestTransitions)
+            }
+
+        rule.setContent {
+            touchSlop = LocalViewConfiguration.current.touchSlop
+            SceneTransitionLayout(
+                state = state,
+                modifier = Modifier.size(layoutWidth, layoutHeight)
+            ) {
+                scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) {
+                    Spacer(modifierSceneA().fillMaxSize())
+                }
+                scene(SceneB, userActions = mapOf(Swipe.Down to SceneA)) {
+                    Spacer(Modifier.fillMaxSize())
+                }
+            }
+        }
+
+        pointerDownAndScrollTouchSlop()
+
+        assertThat(state.transitionState).isIdle()
+
+        return state
+    }
+
+    private fun pointerDownAndScrollTouchSlop() {
+        rule.onRoot().performTouchInput {
+            val middleTop = Offset((layoutWidth / 2).toPx(), 0f)
+            down(middleTop)
+            // Scroll touchSlop
+            moveBy(Offset(0f, touchSlop), delayMillis = 1_000)
+        }
+    }
+
+    private fun scrollDown(percent: Float = 1f) {
+        rule.onRoot().performTouchInput {
+            moveBy(Offset(0f, layoutHeight.toPx() * percent), delayMillis = 1_000)
+        }
+    }
+
+    private fun scrollUp(percent: Float = 1f) = scrollDown(-percent)
+
+    private fun pointerUp() {
+        rule.onRoot().performTouchInput { up() }
+    }
+
+    @Test
+    fun scrollableElementsInSTL_shouldHavePriority() {
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier
+                // A scrollable that consumes the scroll gesture
+                .scrollable(rememberScrollableState { it }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+
+        // Consumed by the scrollable element
+        assertThat(state.transitionState).isIdle()
+    }
+
+    @Test
+    fun unconsumedScrollEvents_canBeConsumedBySTLByDefault() {
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier
+                // A scrollable that does not consume the scroll gesture
+                .scrollable(rememberScrollableState { 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        // STL will start a transition with the remaining scroll
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+
+        scrollUp(percent = 1f)
+        assertThat(transition).hasProgress(1.5f)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_DuringTransitionBetweenScenes() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes
+                )
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        pointerUp()
+        assertThat(state.transitionState).isIdle()
+
+        // Start a new gesture
+        pointerDownAndScrollTouchSlop()
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_EdgeNoPreview() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.EdgeNoPreview
+                )
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        pointerUp()
+        assertThat(state.transitionState).isIdle()
+
+        // Start a new gesture
+        pointerDownAndScrollTouchSlop()
+        scrollUp(percent = 0.5f)
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneB)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_EdgeWithPreview() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.EdgeWithPreview
+                )
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        val transition1 = assertThat(state.transitionState).isTransition()
+        assertThat(transition1).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneA)
+
+        // Start a new gesture
+        pointerDownAndScrollTouchSlop()
+        scrollUp(percent = 0.5f)
+        val transition2 = assertThat(state.transitionState).isTransition()
+        assertThat(transition2).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneB)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_EdgeAlways() {
+        var canScroll = true
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier.verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways)
+                .scrollable(rememberScrollableState { if (canScroll) it else 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        assertThat(state.transitionState).isIdle()
+
+        // Reach the end of the scrollable element
+        canScroll = false
+        scrollUp(percent = 0.5f)
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+
+        pointerUp()
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentScene(SceneB)
+    }
+
+    @Test
+    fun customizeStlNestedScrollBehavior_multipleRequests() {
+        val state = setup2ScenesAndScrollTouchSlop {
+            Modifier
+                // This verticalNestedScrollToScene is closer the STL (an ancestor node)
+                .verticalNestedScrollToScene(bottomBehavior = NestedScrollBehavior.EdgeAlways)
+                // Another verticalNestedScrollToScene modifier
+                .verticalNestedScrollToScene(
+                    bottomBehavior = NestedScrollBehavior.DuringTransitionBetweenScenes
+                )
+                .scrollable(rememberScrollableState { 0f }, Vertical)
+        }
+
+        scrollUp(percent = 0.5f)
+        // EdgeAlways always consume the remaining scroll, DuringTransitionBetweenScenes does not.
+        val transition = assertThat(state.transitionState).isTransition()
+        assertThat(transition).hasProgress(0.5f)
+    }
+}