Skip to content

Commit 85d16fa

Browse files
committed
feat(nav3): add NonDraggableRoute and NonDismissableRoute support for inner sheet routes
Use Material3 sheetGesturesEnabled and ModalBottomSheetProperties to block drag gestures and scrim/back dismissal on sheets whose inner route implements these marker interfaces. Inner route state is communicated to the outer ModalBottomSheet via reactive CodeNavigator properties and LocalSheetNavigator composition local. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 21f3fd0 commit 85d16fa

7 files changed

Lines changed: 66 additions & 17 deletions

File tree

apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import androidx.compose.animation.slideOutHorizontally
88
import androidx.compose.animation.togetherWith
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.runtime.DisposableEffect
12+
import androidx.compose.runtime.derivedStateOf
13+
import androidx.compose.runtime.getValue
1114
import androidx.compose.runtime.remember
1215
import androidx.navigation3.runtime.NavEntry
1316
import androidx.navigation3.runtime.NavKey
@@ -53,11 +56,14 @@ import com.flipcash.app.withdrawal.WithdrawalDestinationScreen
5356
import com.flipcash.app.withdrawal.WithdrawalEntryScreen
5457
import com.flipcash.app.withdrawal.WithdrawalFlow
5558
import com.getcode.navigation.AppNavHost
59+
import com.getcode.navigation.NonDismissableRoute
60+
import com.getcode.navigation.NonDraggableRoute
5661
import com.getcode.navigation.annotatedEntry
5762
import com.getcode.navigation.core.LocalCodeNavigator
5863
import com.getcode.navigation.core.rememberCodeNavigator
5964
import com.getcode.navigation.results.NavResultStateRegistry
6065
import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher
66+
import com.getcode.navigation.scenes.LocalSheetNavigator
6167
import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy
6268
import com.getcode.ui.components.bars.BarManager
6369
import dev.theolm.rinku.DeepLink
@@ -187,6 +193,24 @@ private fun SheetContent(
187193
}
188194
}
189195

196+
// Toggle the outer sheet's drag/dismiss behavior based on the current inner route.
197+
val sheetNavigator = LocalSheetNavigator.current
198+
val currentInnerRoute by remember {
199+
derivedStateOf { backStack.lastOrNull() }
200+
}
201+
if (sheetNavigator != null) {
202+
val isDragDisabled = currentInnerRoute is NonDraggableRoute
203+
val isDismissDisabled = currentInnerRoute is NonDismissableRoute
204+
DisposableEffect(isDragDisabled, isDismissDisabled) {
205+
sheetNavigator.sheetDragDisabled = isDragDisabled
206+
sheetNavigator.sheetDismissDisabled = isDismissDisabled
207+
onDispose {
208+
sheetNavigator.sheetDragDisabled = false
209+
sheetNavigator.sheetDismissDisabled = false
210+
}
211+
}
212+
}
213+
190214
CompositionLocalProvider(LocalCodeNavigator provides navigator) {
191215
AppNavHost(
192216
navigator = navigator,

apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import androidx.navigation3.runtime.NavKey
55
import com.flipcash.app.core.money.RegionSelectionKind
66
import com.flipcash.app.core.tokens.TokenPurpose
77
import com.flipcash.app.core.tokens.TokenSwapPurpose
8+
import com.getcode.navigation.NonDismissableRoute
9+
import com.getcode.navigation.NonDraggableRoute
810
import com.getcode.navigation.Sheet
911
import com.getcode.opencode.internal.solana.model.SwapId
1012
import com.getcode.opencode.model.financial.Fiat
@@ -81,7 +83,7 @@ sealed interface AppRoute : NavKey, Parcelable {
8183
@Serializable data class Info(val mint: Mint, val forNeededFunds: Boolean = false, val fromDeeplink: Boolean = false): Token
8284
@Serializable data class Transactions(val mint: Mint): Token
8385
@Serializable data class SwapTransact(val purpose: TokenSwapPurpose, val forNeededFunds: Boolean = false): Token
84-
@Serializable data class TxProcessing(val swapId: SwapId, val awaitExternalWallet: Boolean = false): Token
86+
@Serializable data class TxProcessing(val swapId: SwapId, val awaitExternalWallet: Boolean = false): Token, NonDismissableRoute, NonDraggableRoute
8587
@Serializable data object SellReceipt: Token
8688
}
8789

apps/flipcash/features/tokens/src/main/kotlin/com/flipcash/app/tokens/TokenTxProcessingScreen.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import com.flipcash.app.tokens.ui.BuySellSwapTokenViewModel.Event
1717
import com.getcode.navigation.core.LocalCodeNavigator
1818
import com.getcode.navigation.extensions.flowScopedViewModel
1919
import com.getcode.opencode.internal.solana.model.SwapId
20-
import com.getcode.ui.utils.DisableSheetGestures
2120
import com.getcode.view.LoadingSuccessState
2221
import kotlinx.coroutines.flow.filterIsInstance
2322
import kotlinx.coroutines.flow.firstOrNull
@@ -94,5 +93,4 @@ fun TokenTxProcessingScreen(
9493
}
9594

9695
BackHandler { /* intercept */ }
97-
DisableSheetGestures()
9896
}

ui/navigation/src/main/kotlin/com/getcode/navigation/NavMetadata.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import kotlin.reflect.full.isSuperclassOf
1111

1212
enum class NavMetadataKeys(val key: String, ) {
1313
IsNonDismissable("non_dismissable"),
14+
IsNonDraggable("non_draggable"),
1415
IsSheet("sheet"),
1516
IsSolitarySheet("sheet_solitary"),
1617
NavResultKey("navresult_key"),
@@ -36,6 +37,7 @@ fun KClass<*>.metadata(): Map<String, Any> {
3637
NavMetadataKeys.IsSheet.key to Sheet::class.java.isAssignableFrom(this.java),
3738
NavMetadataKeys.IsSolitarySheet.key to SolitarySheet::class.java.isAssignableFrom(this.java),
3839
NavMetadataKeys.IsNonDismissable.key to NonDismissableRoute::class.java.isAssignableFrom(this.java),
40+
NavMetadataKeys.IsNonDraggable.key to NonDraggableRoute::class.java.isAssignableFrom(this.java),
3941
NavMetadataKeys.NavResultKey.key to (if (NavigationRetVal::class.isSuperclassOf(this)) {
4042
@Suppress("UNCHECKED_CAST")
4143
NavResultKey(

ui/navigation/src/main/kotlin/com/getcode/navigation/Types.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ import androidx.navigation3.runtime.NavKey
44

55
interface Sheet: NavKey
66
interface NonDismissableRoute: NavKey
7+
interface NonDraggableRoute: NavKey
78
interface SolitarySheet: NavKey

ui/navigation/src/main/kotlin/com/getcode/navigation/core/CodeNavigator.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ data class CodeNavigator(
6363
* when the same route key is reused (otherwise Compose reuses the old Hidden
6464
* SheetState because onBack + navigateTo happen in the same snapshot).
6565
*/
66+
/**
67+
* When true, the enclosing bottom sheet disables drag gestures.
68+
* Inner sheet content sets this based on the current route's metadata.
69+
*/
70+
var sheetDragDisabled by mutableStateOf(false)
71+
72+
/**
73+
* When true, the enclosing bottom sheet blocks scrim tap and back press dismissal.
74+
* Inner sheet content sets this based on the current route's metadata.
75+
*/
76+
var sheetDismissDisabled by mutableStateOf(false)
77+
6678
var sheetGeneration by mutableIntStateOf(0)
6779

6880
val currentRouteKey: NavKey?
@@ -195,7 +207,7 @@ data class CodeNavigator(
195207
}
196208

197209
/** Hide/dismiss a sheet (pops the current route). */
198-
fun hide() = navigateBack()
210+
fun hide() = popAll()
199211

200212
/** Replace the current route with a new one. */
201213
fun replace(route: NavKey) = navigate(route, NavOptions(popUpTo = NavOptions.PopUpTo.PopLast))

ui/navigation/src/main/kotlin/com/getcode/navigation/scenes/ModalBottomSheetSceneStrategy.kt

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api
99
import androidx.compose.material3.ModalBottomSheet
1010
import androidx.compose.material3.ModalBottomSheetProperties
1111
import androidx.compose.material3.SheetState
12-
import androidx.compose.material3.SheetValue
1312
import androidx.compose.material3.rememberModalBottomSheetState
1413
import androidx.compose.runtime.Composable
1514
import androidx.compose.runtime.CompositionLocalProvider
1615
import androidx.compose.runtime.LaunchedEffect
16+
import androidx.compose.runtime.ProvidableCompositionLocal
1717
import androidx.compose.runtime.SideEffect
1818
import androidx.compose.runtime.key
1919
import androidx.compose.runtime.rememberCoroutineScope
@@ -27,6 +27,7 @@ import androidx.navigation3.scene.Scene
2727
import androidx.navigation3.scene.SceneStrategy
2828
import androidx.navigation3.scene.SceneStrategyScope
2929
import com.getcode.navigation.NavMetadataKeys
30+
import com.getcode.navigation.core.CodeNavigator
3031
import com.getcode.navigation.core.LocalCodeNavigator
3132
import com.getcode.navigation.results.NavResultKey
3233
import com.getcode.navigation.results.NavResultOrCanceled
@@ -64,7 +65,8 @@ internal class ModalBottomSheetScene<T : Any> @OptIn(ExperimentalMaterial3Api::c
6465
val navigator = LocalCodeNavigator.current
6566
key(key, navigator.sheetGeneration) {
6667
val isNonDismissable =
67-
(metadata[NavMetadataKeys.IsNonDismissable.key] as? Boolean) ?: false
68+
(metadata[NavMetadataKeys.IsNonDismissable.key] as? Boolean ?: false)
69+
|| navigator.sheetDismissDisabled
6870

6971
val handleBackResult = {
7072
val navResultKey =
@@ -82,10 +84,6 @@ internal class ModalBottomSheetScene<T : Any> @OptIn(ExperimentalMaterial3Api::c
8284

8385
var sheetState: SheetState = rememberModalBottomSheetState(
8486
skipPartiallyExpanded = true,
85-
confirmValueChange = { value ->
86-
// prevent dismissing via gesture if non-dismissable
87-
!(value == SheetValue.Hidden && isNonDismissable)
88-
},
8987
)
9088

9189
// Ensure the sheet shows when entering composition. Material3's internal
@@ -133,9 +131,15 @@ internal class ModalBottomSheetScene<T : Any> @OptIn(ExperimentalMaterial3Api::c
133131
// Remove grab bar for bleed to top edge of sheet
134132
ModalBottomSheet(
135133
sheetState = sheetState,
136-
onDismissRequest = { dismiss(false) },
134+
onDismissRequest = { if (!isNonDismissable) dismiss(false) },
135+
sheetGesturesEnabled = !navigator.sheetDragDisabled,
137136
scrimColor = CodeTheme.colors.scrim,
138-
properties = modalBottomSheetProperties,
137+
properties = if (isNonDismissable) {
138+
ModalBottomSheetProperties(
139+
shouldDismissOnBackPress = false,
140+
shouldDismissOnClickOutside = false,
141+
)
142+
} else modalBottomSheetProperties,
139143
dragHandle = null,
140144
contentWindowInsets = { WindowInsets() },
141145
containerColor = CodeTheme.colors.surface,
@@ -154,11 +158,10 @@ internal class ModalBottomSheetScene<T : Any> @OptIn(ExperimentalMaterial3Api::c
154158
.fillMaxWidth()
155159
.fillMaxHeight(CodeTheme.dimens.modalHeightRatio)
156160
) {
157-
CompositionLocalProvider(LocalBottomSheetDismissDispatcher provides {
158-
dismiss(
159-
true
160-
)
161-
}) {
161+
CompositionLocalProvider(
162+
LocalBottomSheetDismissDispatcher provides { dismiss(true) },
163+
LocalSheetNavigator provides navigator,
164+
) {
162165
entry.Content()
163166
}
164167
}
@@ -235,3 +238,10 @@ class ModalBottomSheetSceneStrategy<T : Any>(
235238
}
236239

237240
val LocalBottomSheetDismissDispatcher = staticCompositionLocalOf { {} }
241+
242+
/**
243+
* Provides the outer (root) [CodeNavigator] to inner sheet content.
244+
* Used to toggle [CodeNavigator.sheetDragDisabled] from within nested navigators.
245+
*/
246+
val LocalSheetNavigator: ProvidableCompositionLocal<CodeNavigator?> =
247+
staticCompositionLocalOf { null }

0 commit comments

Comments
 (0)