11package com.getcode.ui.utils
22
3+ import androidx.compose.foundation.ScrollState
34import androidx.compose.foundation.lazy.LazyListState
45import androidx.compose.runtime.Composable
56import androidx.compose.runtime.derivedStateOf
@@ -56,15 +57,12 @@ internal class SheetResignmentConnection(
5657}
5758
5859private class SheetResignmentModifierNode (
59- private val listState : LazyListState ,
60+ private val isAtTopProvider : () -> Boolean ,
6061 private val connection : SheetResignmentConnection ,
6162 private val autoResetDelayMs : Long ,
6263) : Modifier.Node(), DelegatableNode {
6364
64- private val isAtTop by derivedStateOf {
65- listState.firstVisibleItemIndex == 0 &&
66- listState.firstVisibleItemScrollOffset == 0
67- }
65+ private val isAtTop by derivedStateOf { isAtTopProvider() }
6866
6967 private var observeJob: Job ? = null
7068
@@ -96,18 +94,17 @@ private class SheetResignmentModifierNode(
9694}
9795
9896private data class SheetResignmentElement (
99- val listState : LazyListState ,
97+ val isAtTopProvider : () -> Boolean ,
10098 val connection : SheetResignmentConnection ,
10199 val autoResetDelayMs : Long ,
102100) : ModifierNodeElement<SheetResignmentModifierNode>() {
103101
104- override fun create () = SheetResignmentModifierNode (listState , connection, autoResetDelayMs)
102+ override fun create () = SheetResignmentModifierNode (isAtTopProvider , connection, autoResetDelayMs)
105103
106104 override fun update (node : SheetResignmentModifierNode ) = Unit
107105
108106 override fun InspectorInfo.inspectableProperties () {
109107 name = " sheetResignmentBehavior"
110- properties[" listState" ] = listState
111108 properties[" autoResetDelayMs" ] = autoResetDelayMs
112109 }
113110}
@@ -135,5 +132,41 @@ fun Modifier.sheetResignmentBehavior(
135132 }
136133 return this
137134 .nestedScroll(connection)
138- .then(SheetResignmentElement (listState, connection, autoResetDelayMs))
135+ .then(SheetResignmentElement (
136+ isAtTopProvider = {
137+ listState.firstVisibleItemIndex == 0 &&
138+ listState.firstVisibleItemScrollOffset == 0
139+ },
140+ connection = connection,
141+ autoResetDelayMs = autoResetDelayMs,
142+ ))
143+ }
144+
145+ /* *
146+ * Prevents a [ModalBottomSheet] from starting dismiss on the first downward drag after the
147+ * internal [ScrollState] reaches the top. The first drag triggers overscroll bounce only;
148+ * a subsequent downward drag (or after [autoResetDelayMs]) will dismiss normally.
149+ *
150+ * Requires [LocalSheetGesturesState] to be provided, or pass [setGesturesEnabled] explicitly.
151+ *
152+ * @param scrollState the [ScrollState] of the scrollable Column inside the sheet
153+ * @param setGesturesEnabled callback to toggle sheet gesture handling (e.g. `sheetState.gesturesEnabled`)
154+ * @param autoResetDelayMs time after which a paused-then-drag will re-enable dismiss; 0 to disable
155+ */
156+ @Composable
157+ fun Modifier.sheetResignmentBehavior (
158+ scrollState : ScrollState ,
159+ setGesturesEnabled : (Boolean ) -> Unit = LocalSheetGesturesState .current,
160+ autoResetDelayMs : Long = 700L,
161+ ): Modifier {
162+ val connection = remember(setGesturesEnabled, autoResetDelayMs) {
163+ SheetResignmentConnection (setGesturesEnabled, autoResetDelayMs)
164+ }
165+ return this
166+ .nestedScroll(connection)
167+ .then(SheetResignmentElement (
168+ isAtTopProvider = { scrollState.value == 0 },
169+ connection = connection,
170+ autoResetDelayMs = autoResetDelayMs,
171+ ))
139172}
0 commit comments