[go: nahoru, domu]

Add Navigation Material from Accompanists to androidx

Now that the needed compose material API are stable, we can move the
navigation material module from Accompanists to Androidx. This means we
will have a new compose-material-navigation module that provides support
for bottomsheets.

RelNote: "Providing new Compose Material Navigation module that adds
support for Bottomsheets in Compose Material."
Test: Added tests and samples
Bug: 180247978

Change-Id: Ia93eb757a32b04dac8ab3ebd2d73207a68635b80
diff --git a/compose/material/material-navigation/api/current.txt b/compose/material/material-navigation/api/current.txt
new file mode 100644
index 0000000..8255b23
--- /dev/null
+++ b/compose/material/material-navigation/api/current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.compose.material.navigation {
+
+  public final class BottomSheetKt {
+    method @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  @androidx.navigation.Navigator.Name("bottomSheet") public final class BottomSheetNavigator extends androidx.navigation.Navigator<androidx.compose.material.navigation.BottomSheetNavigator.Destination> {
+    ctor public BottomSheetNavigator(androidx.compose.material.ModalBottomSheetState sheetState);
+    method public androidx.compose.material.navigation.BottomSheetNavigator.Destination createDestination();
+    method public androidx.compose.material.navigation.BottomSheetNavigatorSheetState getNavigatorSheetState();
+    property public final androidx.compose.material.navigation.BottomSheetNavigatorSheetState navigatorSheetState;
+  }
+
+  @androidx.navigation.NavDestination.ClassType(Composable::class) public static final class BottomSheetNavigator.Destination extends androidx.navigation.NavDestination implements androidx.navigation.FloatingWindow {
+    ctor public BottomSheetNavigator.Destination(androidx.compose.material.navigation.BottomSheetNavigator navigator, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.ColumnScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+  }
+
+  public final class BottomSheetNavigatorKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.material.navigation.BottomSheetNavigator rememberBottomSheetNavigator(optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec);
+  }
+
+  public final class BottomSheetNavigatorSheetState {
+    ctor public BottomSheetNavigatorSheetState(androidx.compose.material.ModalBottomSheetState sheetState);
+    method public androidx.compose.material.ModalBottomSheetValue getCurrentValue();
+    method public androidx.compose.material.ModalBottomSheetValue getTargetValue();
+    method public boolean isVisible();
+    property public final androidx.compose.material.ModalBottomSheetValue currentValue;
+    property public final boolean isVisible;
+    property public final androidx.compose.material.ModalBottomSheetValue targetValue;
+  }
+
+  public final class NavGraphBuilderKt {
+    method public static void bottomSheet(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.ColumnScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+  }
+
+}
+
diff --git a/compose/material/material-navigation/api/res-current.txt b/compose/material/material-navigation/api/res-current.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compose/material/material-navigation/api/res-current.txt
diff --git a/compose/material/material-navigation/api/restricted_current.txt b/compose/material/material-navigation/api/restricted_current.txt
new file mode 100644
index 0000000..8255b23
--- /dev/null
+++ b/compose/material/material-navigation/api/restricted_current.txt
@@ -0,0 +1,38 @@
+// Signature format: 4.0
+package androidx.compose.material.navigation {
+
+  public final class BottomSheetKt {
+    method @androidx.compose.runtime.Composable public static void ModalBottomSheetLayout(androidx.compose.material.navigation.BottomSheetNavigator bottomSheetNavigator, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional long scrimColor, kotlin.jvm.functions.Function0<kotlin.Unit> content);
+  }
+
+  @androidx.navigation.Navigator.Name("bottomSheet") public final class BottomSheetNavigator extends androidx.navigation.Navigator<androidx.compose.material.navigation.BottomSheetNavigator.Destination> {
+    ctor public BottomSheetNavigator(androidx.compose.material.ModalBottomSheetState sheetState);
+    method public androidx.compose.material.navigation.BottomSheetNavigator.Destination createDestination();
+    method public androidx.compose.material.navigation.BottomSheetNavigatorSheetState getNavigatorSheetState();
+    property public final androidx.compose.material.navigation.BottomSheetNavigatorSheetState navigatorSheetState;
+  }
+
+  @androidx.navigation.NavDestination.ClassType(Composable::class) public static final class BottomSheetNavigator.Destination extends androidx.navigation.NavDestination implements androidx.navigation.FloatingWindow {
+    ctor public BottomSheetNavigator.Destination(androidx.compose.material.navigation.BottomSheetNavigator navigator, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.ColumnScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+  }
+
+  public final class BottomSheetNavigatorKt {
+    method @androidx.compose.runtime.Composable public static androidx.compose.material.navigation.BottomSheetNavigator rememberBottomSheetNavigator(optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec);
+  }
+
+  public final class BottomSheetNavigatorSheetState {
+    ctor public BottomSheetNavigatorSheetState(androidx.compose.material.ModalBottomSheetState sheetState);
+    method public androidx.compose.material.ModalBottomSheetValue getCurrentValue();
+    method public androidx.compose.material.ModalBottomSheetValue getTargetValue();
+    method public boolean isVisible();
+    property public final androidx.compose.material.ModalBottomSheetValue currentValue;
+    property public final boolean isVisible;
+    property public final androidx.compose.material.ModalBottomSheetValue targetValue;
+  }
+
+  public final class NavGraphBuilderKt {
+    method public static void bottomSheet(androidx.navigation.NavGraphBuilder, String route, optional java.util.List<androidx.navigation.NamedNavArgument> arguments, optional java.util.List<androidx.navigation.NavDeepLink> deepLinks, kotlin.jvm.functions.Function2<? super androidx.compose.foundation.layout.ColumnScope,? super androidx.navigation.NavBackStackEntry,kotlin.Unit> content);
+  }
+
+}
+
diff --git a/compose/material/material-navigation/build.gradle b/compose/material/material-navigation/build.gradle
new file mode 100644
index 0000000..b32975c
--- /dev/null
+++ b/compose/material/material-navigation/build.gradle
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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.
+ */
+
+import androidx.build.Publish
+import androidx.build.RunApiTasks
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    api("androidx.navigation:navigation-compose:2.7.7")
+    implementation(project(":compose:material:material"))
+    implementation(libs.kotlinStdlib)
+
+    androidTestImplementation project(":compose:test-utils")
+    androidTestImplementation("androidx.navigation:navigation-testing:2.7.7")
+    androidTestImplementation(project(":compose:ui:ui-test-junit4"))
+    androidTestImplementation(project(":compose:ui:ui-test-manifest"))
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.junit)
+    androidTestImplementation(libs.truth)
+    androidTestImplementation(libs.testRules)
+}
+
+androidx {
+    name = "Compose Material Navigation"
+    publish = Publish.SNAPSHOT_AND_RELEASE
+    inceptionYear = "2024"
+    description = "Compose Material integration with Navigation"
+    samples(projectOrArtifact(":compose:material:material-navigation-samples"))
+}
+
+android {
+    namespace "androidx.compose.material.navigation"
+}
diff --git a/compose/material/material-navigation/samples/build.gradle b/compose/material/material-navigation/samples/build.gradle
new file mode 100644
index 0000000..7e3acd6
--- /dev/null
+++ b/compose/material/material-navigation/samples/build.gradle
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2024 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.
+ */
+
+/**
+ * This file was created using the `create_project.py` script located in the
+ * `<AndroidX root>/development/project-creator` directory.
+ *
+ * Please use that script when creating a new project, rather than copying an existing project and
+ * modifying its settings.
+ */
+import androidx.build.LibraryType
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("AndroidXComposePlugin")
+    id("org.jetbrains.kotlin.android")
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+
+    compileOnly(project(":annotation:annotation-sampled"))
+    implementation(project(":compose:foundation:foundation"))
+    implementation(project(":compose:material:material"))
+    implementation(project(":compose:material:material-navigation"))
+}
+
+androidx {
+    name = "Compose Material Navigation Integration Samples"
+    type = LibraryType.SAMPLES
+    inceptionYear = "2024"
+    description = "Samples for Compose Material integration with Navigation"
+}
+
+android {
+    namespace "androidx.compose.material.navigation.samples"
+}
diff --git a/compose/material/material-navigation/samples/src/main/java/androidx/compose/material/navigation/samples/ComposeMaterialNavigationSamples.kt b/compose/material/material-navigation/samples/src/main/java/androidx/compose/material/navigation/samples/ComposeMaterialNavigationSamples.kt
new file mode 100644
index 0000000..bc17bef
--- /dev/null
+++ b/compose/material/material-navigation/samples/src/main/java/androidx/compose/material/navigation/samples/ComposeMaterialNavigationSamples.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2020 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.material.navigation.samples
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.material.navigation.ModalBottomSheetLayout
+import androidx.compose.material.navigation.bottomSheet
+import androidx.compose.material.navigation.rememberBottomSheetNavigator
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import java.util.UUID
+
+private object Destinations {
+    const val Home = "HOME"
+    const val Feed = "FEED"
+    const val Sheet = "SHEET"
+}
+
+@Composable
+fun BottomSheetNavDemo() {
+    val bottomSheetNavigator = rememberBottomSheetNavigator()
+    val navController = rememberNavController(bottomSheetNavigator)
+
+    ModalBottomSheetLayout(bottomSheetNavigator) {
+        NavHost(navController, Destinations.Home) {
+            composable(Destinations.Home) {
+                HomeScreen(
+                    showSheet = {
+                        navController.navigate(Destinations.Sheet + "?arg=From Home Screen")
+                    },
+                    showFeed = { navController.navigate(Destinations.Feed) }
+                )
+            }
+            composable(Destinations.Feed) { Text("Feed!") }
+            bottomSheet(Destinations.Sheet + "?arg={arg}") { backstackEntry ->
+                val arg = backstackEntry.arguments?.getString("arg") ?: "Missing argument :("
+                BottomSheet(
+                    showFeed = { navController.navigate(Destinations.Feed) },
+                    showAnotherSheet = {
+                        navController.navigate(Destinations.Sheet + "?arg=${UUID.randomUUID()}")
+                    },
+                    arg = arg
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun HomeScreen(showSheet: () -> Unit, showFeed: () -> Unit) {
+    Column(Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
+        Text("Body")
+        Button( {
+            Text("Show sheet!")
+        }
+        Button( {
+            Text("Navigate to Feed")
+        }
+    }
+}
+
+@Composable
+private fun BottomSheet(showFeed: () -> Unit, showAnotherSheet: () -> Unit, arg: String) {
+    Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
+        Text("Sheet with arg: $arg")
+        Button( {
+            Text("Click me to navigate!")
+        }
+        Button( {
+            Text("Click me to show another sheet!")
+        }
+    }
+}
diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt b/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt
new file mode 100644
index 0000000..964957f
--- /dev/null
+++ b/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/BottomSheetNavigatorTest.kt
@@ -0,0 +1,862 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import android.os.Bundle
+import androidx.activity.OnBackPressedDispatcher
+import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.Button
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.Text
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.click
+import androidx.compose.ui.test.getUnclippedBoundsInRoot
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.height
+import androidx.lifecycle.Lifecycle
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import androidx.navigation.testing.TestNavigatorState
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.math.roundToLong
+import kotlinx.coroutines.runBlocking
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+internal class BottomSheetNavigatorTest {
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun testNavigateAddsDestinationToBackStack(): Unit = runBlocking {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val navigatorState = TestNavigatorState()
+        val navigator = BottomSheetNavigator(sheetState)
+
+        navigator.onAttach(navigatorState)
+        val entry = navigatorState.createBackStackEntry(navigator.createFakeDestination(), null)
+        navigator.navigate(listOf(entry), null, null)
+
+        assertWithMessage("The back stack entry has been added to the back stack")
+            .that(navigatorState.backStack.value)
+            .containsExactly(entry)
+    }
+
+    @Test
+    fun testNavigateAddsDestinationToBackStackAndKeepsPrevious(): Unit = runBlocking {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val navigator = BottomSheetNavigator(sheetState)
+        val navigatorState = TestNavigatorState()
+
+        navigator.onAttach(navigatorState)
+        val firstEntry =
+            navigatorState.createBackStackEntry(navigator.createFakeDestination(), null)
+        val secondEntry =
+            navigatorState.createBackStackEntry(navigator.createFakeDestination(), null)
+
+        navigator.navigate(listOf(firstEntry), null, null)
+        assertWithMessage("The first entry has been added to the back stack")
+            .that(navigatorState.backStack.value)
+            .containsExactly(firstEntry)
+
+        navigator.navigate(listOf(secondEntry), null, null)
+        assertWithMessage(
+            "The second entry has been added to the back stack and it still " +
+                "contains the first entry"
+        )
+            .that(navigatorState.backStack.value)
+            .containsExactly(firstEntry, secondEntry)
+            .inOrder()
+    }
+
+    @Test
+    fun testNavigateComposesDestinationAndDisposesPreviousDestination(): Unit = runBlocking {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val navigator = BottomSheetNavigator(sheetState)
+        val navigatorState = TestNavigatorState()
+        navigator.onAttach(navigatorState)
+
+        composeTestRule.setContent {
+            Column { navigator.sheetContent(this) }
+        }
+
+        var firstDestinationCompositions = 0
+        val firstDestinationContentTag = "firstSheetContentTest"
+        val firstDestination = BottomSheetNavigator.Destination(navigator) {
+            DisposableEffect(Unit) {
+                firstDestinationCompositions++
+                onDispose { firstDestinationCompositions = 0 }
+            }
+            Text("Fake Sheet Content", Modifier.testTag(firstDestinationContentTag))
+        }
+        val firstEntry = navigatorState.createBackStackEntry(firstDestination, null)
+
+        var secondDestinationCompositions = 0
+        val secondDestinationContentTag = "secondSheetContentTest"
+        val secondDestination = BottomSheetNavigator.Destination(navigator) {
+            DisposableEffect(Unit) {
+                secondDestinationCompositions++
+                onDispose { secondDestinationCompositions = 0 }
+            }
+            Box(
+                Modifier
+                    .size(64.dp)
+                    .testTag(secondDestinationContentTag)
+            )
+        }
+        val secondEntry = navigatorState.createBackStackEntry(secondDestination, null)
+
+        navigator.navigate(listOf(firstEntry), null, null)
+        composeTestRule.awaitIdle()
+
+        composeTestRule.onNodeWithTag(firstDestinationContentTag).assertExists()
+        composeTestRule.onNodeWithTag(secondDestinationContentTag).assertDoesNotExist()
+        assertWithMessage("First destination should have been composed exactly once")
+            .that(firstDestinationCompositions).isEqualTo(1)
+        assertWithMessage("Second destination should not have been composed yet")
+            .that(secondDestinationCompositions).isEqualTo(0)
+
+        navigator.navigate(listOf(secondEntry), null, null)
+        composeTestRule.awaitIdle()
+
+        composeTestRule.onNodeWithTag(firstDestinationContentTag).assertDoesNotExist()
+        composeTestRule.onNodeWithTag(secondDestinationContentTag).assertExists()
+        assertWithMessage("First destination has not been disposed")
+            .that(firstDestinationCompositions).isEqualTo(0)
+        assertWithMessage("Second destination should have been composed exactly once")
+            .that(secondDestinationCompositions).isEqualTo(1)
+    }
+
+    @Test
+    fun testBackStackEntryPoppedAfterManualSheetDismiss(): Unit = runBlocking {
+        val navigatorState = TestNavigatorState()
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val navigator = BottomSheetNavigator(sheetState = sheetState)
+        navigator.onAttach(navigatorState)
+
+        val bodyContentTag = "testBodyContent"
+
+        composeTestRule.setContent {
+            ModalBottomSheetLayout(
+                bottomSheetNavigator = navigator,
+                content = {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(bodyContentTag)
+                    )
+                }
+            )
+        }
+
+        val destination = BottomSheetNavigator.Destination(
+            navigator = navigator,
+            content = { Box(Modifier.height(20.dp)) }
+        )
+        val backStackEntry = navigatorState.createBackStackEntry(destination, null)
+        navigator.navigate(listOf(backStackEntry), null, null)
+        composeTestRule.awaitIdle()
+
+        assertWithMessage("Navigated to destination")
+            .that(navigatorState.backStack.value)
+            .containsExactly(backStackEntry)
+        assertWithMessage("Bottom sheet shown")
+            .that(sheetState.isVisible).isTrue()
+
+        composeTestRule.onNodeWithTag(bodyContentTag).performClick()
+        composeTestRule.awaitIdle()
+        assertWithMessage("Sheet should be hidden")
+            .that(sheetState.isVisible).isFalse()
+        assertThat(navigatorState.transitionsInProgress.value).isEmpty()
+        assertWithMessage("Back stack entry should be popped off the back stack")
+            .that(navigatorState.backStack.value)
+            .isEmpty()
+    }
+
+    @Test
+    fun testSheetShownAfterNavControllerRestoresState() = runBlocking {
+        lateinit var navController: NavHostController
+        lateinit var navigator: BottomSheetNavigator
+        var savedState: Bundle? = null
+        var compositionState by mutableStateOf(0)
+
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val textInSheetTag = "textInSheet"
+
+        composeTestRule.setContent {
+            navigator = remember { BottomSheetNavigator(sheetState) }
+            navController = rememberNavController(navigator)
+            if (savedState != null) navController.restoreState(savedState)
+            if (compositionState == 0) {
+                ModalBottomSheetLayout(
+                    bottomSheetNavigator = navigator
+                ) {
+                    NavHost(navController, startDestination = "first") {
+                        bottomSheet("first") {
+                            Text("Hello!", Modifier.testTag(textInSheetTag))
+                        }
+                    }
+                }
+            }
+        }
+
+        savedState = navController.saveState()
+
+        // Dispose the ModalBottomSheetLayout
+        compositionState = 1
+        composeTestRule.awaitIdle()
+
+        composeTestRule.onNodeWithTag(textInSheetTag).assertDoesNotExist()
+
+        // Recompose with the ModalBottomSheetLayout
+        compositionState = 0
+        composeTestRule.awaitIdle()
+
+        assertWithMessage("Destination is first destination")
+            .that(navController.currentDestination?.route)
+            .isEqualTo("first")
+        assertWithMessage("Bottom sheet is visible")
+            .that(sheetState.isVisible).isTrue()
+    }
+
+    @Test
+    fun testNavigateCompletesEntriesTransitions() = runBlocking {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val navigator = BottomSheetNavigator(sheetState)
+        val navigatorState = TestNavigatorState()
+
+        navigator.onAttach(navigatorState)
+
+        composeTestRule.setContent {
+            ModalBottomSheetLayout(
+                bottomSheetNavigator = navigator,
+                content = { Box(Modifier.fillMaxSize()) }
+            )
+        }
+
+        val backStackEntry1 = navigatorState.createBackStackEntry(
+            navigator.createFakeDestination(), null
+        )
+        val backStackEntry2 = navigatorState.createBackStackEntry(
+            navigator.createFakeDestination(), null
+        )
+
+        navigator.navigate(
+            entries = listOf(backStackEntry1, backStackEntry2),
+            navOptions = null,
+            navigatorExtras = null
+        )
+
+        composeTestRule.awaitIdle()
+
+        assertThat(navigatorState.transitionsInProgress.value).doesNotContain(backStackEntry1)
+        assertThat(navigatorState.transitionsInProgress.value).doesNotContain(backStackEntry2)
+        assertThat(backStackEntry2.lifecycle.currentState).isEqualTo(Lifecycle.State.RESUMED)
+    }
+
+    @Test
+    fun testComposeSheetContentBeforeNavigatorAttached(): Unit = runBlocking {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val navigator = BottomSheetNavigator(sheetState)
+        val navigatorState = TestNavigatorState()
+
+        composeTestRule.setContent {
+            ModalBottomSheetLayout(
+                bottomSheetNavigator = navigator,
+                content = { Box(Modifier.fillMaxSize()) }
+            )
+        }
+
+        // Attach the state only after accessing the navigator's sheetContent in
+        // ModalBottomSheetLayout
+        navigator.onAttach(navigatorState)
+
+        val entry = navigatorState.createBackStackEntry(
+            navigator.createFakeDestination(), null
+        )
+
+        navigator.navigate(
+            entries = listOf(entry),
+            navOptions = null,
+            navigatorExtras = null
+        )
+
+        composeTestRule.awaitIdle()
+
+        assertWithMessage("The back stack entry has been added to the back stack")
+            .that(navigatorState.backStack.value)
+            .containsExactly(entry)
+    }
+
+    @Test
+    fun testBackPressedDestroysEntry() {
+        lateinit var onBackPressedDispatcher: OnBackPressedDispatcher
+        lateinit var navController: NavHostController
+
+        composeTestRule.setContent {
+            val bottomSheetNavigator = rememberBottomSheetNavigator()
+            navController = rememberNavController(bottomSheetNavigator)
+            >
+                LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher!!
+
+            ModalBottomSheetLayout(bottomSheetNavigator) {
+                Box(modifier = Modifier.fillMaxSize()) {
+                    NavHost(
+                        navController = navController,
+                        startDestination = "mainScreen"
+                    ) {
+
+                        composable(
+                            route = "mainScreen",
+                            content = {
+                                Button( navController.navigate("bottomSheet") }) {
+                                    Text(text = "open drawer")
+                                }
+                            }
+                        )
+
+                        bottomSheet(
+                            route = "bottomSheet",
+                            content = {
+                                Box(modifier = Modifier.fillMaxSize()) {
+                                    Text(
+                                        text = "bottomSheet"
+                                    )
+                                }
+                            }
+                        )
+                    }
+                }
+            }
+        }
+
+        composeTestRule.onNodeWithText("open drawer").performClick()
+
+        lateinit var bottomSheetEntry: NavBackStackEntry
+
+        composeTestRule.runOnIdle {
+            bottomSheetEntry = navController.currentBackStackEntry!!
+            onBackPressedDispatcher.onBackPressed()
+        }
+
+        composeTestRule.runOnIdle {
+            assertThat(bottomSheetEntry.lifecycle.currentState).isEqualTo(Lifecycle.State.DESTROYED)
+        }
+    }
+
+    @Test
+    fun testSheetContentSizeChangeDuringAnimation_opensSheet_shortSheetToShortSheet() {
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        var height: Dp by mutableStateOf(20.dp)
+        lateinit var sheetNavBackStackEntry: NavBackStackEntry
+        val homeDestination = "home"
+        val sheetDestination = "sheet"
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator()
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        Box(
+                            Modifier
+                                .fillMaxSize()
+                                .background(Color.Blue)
+                        )
+                    }
+                    bottomSheet(sheetDestination) { backStackEntry ->
+                        sheetNavBackStackEntry = backStackEntry
+                        Box(
+                            Modifier
+                                .height(height)
+                                .fillMaxWidth()
+                                .background(Color.Red)
+                        )
+                    }
+                }
+            }
+        }
+
+        composeTestRule.mainClock.autoAdvance = false
+        composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
+        composeTestRule.mainClock.advanceTimeBy(100)
+
+        assertThat(navigator.transitionsInProgress.value.lastOrNull())
+            .isEqualTo(sheetNavBackStackEntry)
+
+        height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 3
+
+        composeTestRule.mainClock.autoAdvance = true
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isTrue()
+
+        assertThat(navigator.transitionsInProgress.value).isEmpty()
+
+        composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isFalse()
+    }
+
+    @Test
+    fun testSheetContentSizeChangeDuringAnimation_opensSheet_shortSheetToTallSheet() {
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        var height: Dp by mutableStateOf(20.dp)
+        lateinit var sheetNavBackStackEntry: NavBackStackEntry
+        val homeDestination = "home"
+        val sheetDestination = "sheet"
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator()
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        Box(Modifier.fillMaxSize().background(Color.Blue))
+                    }
+                    bottomSheet(sheetDestination) { backStackEntry ->
+                        sheetNavBackStackEntry = backStackEntry
+                        Box(Modifier.height(height).fillMaxWidth().background(Color.Red))
+                    }
+                }
+            }
+        }
+
+        composeTestRule.mainClock.autoAdvance = false
+        composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
+        composeTestRule.mainClock.advanceTimeBy(100)
+        assertThat(navigator.transitionsInProgress.value.lastOrNull())
+            .isEqualTo(sheetNavBackStackEntry)
+
+        height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 0.9f
+
+        composeTestRule.mainClock.autoAdvance = true
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isTrue()
+
+        assertThat(navigator.transitionsInProgress.value).isEmpty()
+
+        composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isFalse()
+    }
+
+    @Test
+    fun testSheetContentSizeChangeDuringAnimation_opensSheet_tallSheetToTallSheet() {
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        lateinit var sheetNavBackStackEntry: NavBackStackEntry
+        var height: Dp by mutableStateOf(0.dp)
+        val homeDestination = "home"
+        val sheetDestination = "sheet"
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator()
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        Box(Modifier.fillMaxSize().background(Color.Blue))
+                    }
+                    bottomSheet(sheetDestination) { backStackEntry ->
+                        sheetNavBackStackEntry = backStackEntry
+                        Box(Modifier.height(height).fillMaxWidth().background(Color.Red))
+                    }
+                }
+            }
+        }
+
+        val rootHeight = composeTestRule.onRoot().getUnclippedBoundsInRoot().height
+        height = rootHeight
+
+        composeTestRule.mainClock.autoAdvance = false
+        composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
+        composeTestRule.mainClock.advanceTimeBy(100)
+        assertThat(navigator.transitionsInProgress.value.lastOrNull())
+            .isEqualTo(sheetNavBackStackEntry)
+
+        height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 0.9f
+
+        composeTestRule.mainClock.autoAdvance = true
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isTrue()
+
+        assertThat(navigator.transitionsInProgress.value).isEmpty()
+
+        composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isFalse()
+    }
+
+    @Test
+    fun testSheetContentSizeChangeDuringAnimation_opensSheet_tallSheetToShortSheet() {
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        var height: Dp by mutableStateOf(0.dp)
+        lateinit var sheetNavBackStackEntry: NavBackStackEntry
+        val homeDestination = "home"
+        val sheetDestination = "sheet"
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator()
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        Box(Modifier.fillMaxSize().background(Color.Blue))
+                    }
+                    bottomSheet(sheetDestination) { backStackEntry ->
+                        sheetNavBackStackEntry = backStackEntry
+                        Box(Modifier.height(height).fillMaxWidth().background(Color.Red))
+                    }
+                }
+            }
+        }
+
+        val rootHeight = composeTestRule.onRoot().getUnclippedBoundsInRoot().height
+        height = rootHeight
+
+        composeTestRule.mainClock.autoAdvance = false
+        composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
+        composeTestRule.mainClock.advanceTimeBy(100)
+        assertThat(navigator.transitionsInProgress.value.lastOrNull())
+            .isEqualTo(sheetNavBackStackEntry)
+
+        height = (composeTestRule.onRoot().getUnclippedBoundsInRoot().height) / 3f
+
+        composeTestRule.mainClock.autoAdvance = true
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isTrue()
+
+        assertThat(navigator.transitionsInProgress.value).isEmpty()
+
+        composeTestRule.runOnUiThread { navController.navigate(homeDestination) }
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isFalse()
+    }
+
+    @OptIn(ExperimentalMaterialApi::class)
+    @Test
+    fun testPopBackStackHidesSheetWithAnimation() {
+        val animationDuration = 2000
+        val animationSpec = tween<Float>(animationDuration)
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator(animationSpec)
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator) {
+                NavHost(navController, "first") {
+                    composable("first") {
+                        Box(Modifier.fillMaxSize())
+                    }
+                    bottomSheet("sheet") {
+                        Box(Modifier.height(200.dp))
+                    }
+                }
+            }
+        }
+
+        composeTestRule.runOnUiThread { navController.navigate("sheet") }
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isTrue()
+
+        composeTestRule.mainClock.autoAdvance = false
+        composeTestRule.runOnUiThread { navController.popBackStack() }
+
+        val firstAnimationTimeBreakpoint = (animationDuration * 0.9).roundToLong()
+
+        composeTestRule.mainClock.advanceTimeBy(firstAnimationTimeBreakpoint)
+        assertThat(navigator.navigatorSheetState.currentValue)
+            .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
+        assertThat(navigator.navigatorSheetState.targetValue)
+            .isEqualTo(ModalBottomSheetValue.Hidden)
+
+        composeTestRule.runOnUiThread { navController.navigate("first") }
+
+        composeTestRule.mainClock.autoAdvance = true
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.currentValue)
+            .isEqualTo(ModalBottomSheetValue.Hidden)
+    }
+
+    @Test
+    fun testTapOnScrimDismissesSheetAndPopsBackStack() {
+        val animationDuration = 2000
+        val animationSpec = tween<Float>(animationDuration)
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        val sheetLayoutTestTag = "sheetLayout"
+        val homeDestination = "home"
+        val sheetDestination = "sheet"
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator(animationSpec)
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator, Modifier.testTag(sheetLayoutTestTag)) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        Box(
+                            Modifier
+                                .fillMaxSize()
+                                .background(Color.Red)
+                        )
+                    }
+                    bottomSheet(sheetDestination) {
+                        Box(
+                            Modifier
+                                .height(200.dp)
+                                .fillMaxWidth()
+                                .background(Color.Green)
+                        ) {
+                            Text("Hello!")
+                        }
+                    }
+                }
+            }
+        }
+
+        assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo(
+            homeDestination
+        )
+        assertThat(navigator.navigatorSheetState.isVisible).isFalse()
+
+        composeTestRule.runOnUiThread { navController.navigate(sheetDestination) }
+        composeTestRule.waitForIdle()
+
+        assertThat(navController.currentBackStackEntry?.destination?.route).isEqualTo(
+            sheetDestination
+        )
+        assertThat(navController.currentBackStackEntry?.lifecycle?.currentState)
+            .isEqualTo(Lifecycle.State.RESUMED)
+        assertThat(navigator.navigatorSheetState.isVisible).isTrue()
+
+        composeTestRule.onNodeWithTag(sheetLayoutTestTag)
+            .performTouchInput { click(position = topCenter) }
+
+        composeTestRule.waitForIdle()
+        assertThat(navigator.navigatorSheetState.isVisible).isFalse()
+    }
+
+    @Test
+    fun testNavigatingFromSheetToSheetDismissesAndThenShowsSheet() {
+        val animationDuration = 2000
+        val animationSpec = tween<Float>(animationDuration)
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        val sheetLayoutTestTag = "sheetLayout"
+        val homeDestination = "home"
+        val firstSheetDestination = "sheet1"
+        val secondSheetDestination = "sheet2"
+
+        composeTestRule.setContent {
+            navigator = rememberBottomSheetNavigator(animationSpec)
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator, Modifier.testTag(sheetLayoutTestTag)) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        Box(
+                            Modifier
+                                .fillMaxSize()
+                                .background(Color.Red)
+                        )
+                    }
+                    bottomSheet(firstSheetDestination) {
+                        Box(
+                            Modifier
+                                .height(200.dp)
+                                .fillMaxWidth()
+                                .background(Color.Green)
+                        ) {
+                            Text("Hello!")
+                        }
+                    }
+                    bottomSheet(secondSheetDestination) {
+                        Box(
+                            Modifier
+                                .height(200.dp)
+                                .fillMaxWidth()
+                                .background(Color.Blue)
+                        ) {
+                            Text("Hello!")
+                        }
+                    }
+                }
+            }
+        }
+
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(homeDestination)
+
+        composeTestRule.runOnUiThread { navController.navigate(firstSheetDestination) }
+        composeTestRule.waitForIdle()
+
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(firstSheetDestination)
+        assertThat(navigator.sheetState.currentValue)
+            .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
+
+        composeTestRule.mainClock.autoAdvance = false
+        composeTestRule.runOnUiThread { navController.navigate(secondSheetDestination) }
+
+        composeTestRule.mainClock.advanceTimeUntil { navigator.sheetState.isAnimationRunning }
+        composeTestRule.mainClock.advanceTimeBy(animationDuration.toLong())
+        composeTestRule.mainClock.advanceTimeByFrame()
+
+        assertThat(navigator.sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+
+        composeTestRule.mainClock.advanceTimeUntil { navigator.sheetState.isAnimationRunning }
+        composeTestRule.mainClock.advanceTimeBy(animationDuration.toLong())
+        composeTestRule.mainClock.advanceTimeByFrame()
+
+        assertThat(navigator.sheetState.currentValue)
+            .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(secondSheetDestination)
+
+        composeTestRule.runOnUiThread {
+            navController.popBackStack(firstSheetDestination, inclusive = false)
+        }
+        composeTestRule.mainClock.advanceTimeBy(animationDuration.toLong())
+        composeTestRule.mainClock.advanceTimeByFrame()
+
+        assertThat(navigator.sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+
+        composeTestRule.mainClock.autoAdvance = true
+        composeTestRule.waitForIdle()
+
+        assertThat(navigator.sheetState.currentValue)
+            .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(firstSheetDestination)
+    }
+
+    @Test
+    fun testBackPressWithNestedGraphBehind() {
+        lateinit var navigator: BottomSheetNavigator
+        lateinit var navController: NavHostController
+        lateinit var nestedNavController: NavHostController
+        lateinit var backDispatcher: OnBackPressedDispatcher
+        val homeDestination = "home"
+        val firstSheetDestination = "sheet1"
+        val firstNestedDestination = "nested1"
+        val secondNestedDestination = "nested2"
+
+        composeTestRule.setContent {
+            backDispatcher = LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher!!
+            navigator = rememberBottomSheetNavigator()
+            navController = rememberNavController(navigator)
+            ModalBottomSheetLayout(navigator) {
+                NavHost(navController, homeDestination) {
+                    composable(homeDestination) {
+                        nestedNavController = rememberNavController()
+                        NavHost(nestedNavController, "nested1") {
+                            composable(firstNestedDestination) { }
+                            composable(secondNestedDestination) { }
+                        }
+                    }
+                    bottomSheet(firstSheetDestination) {
+                        Text("SheetDestination")
+                    }
+                }
+            }
+        }
+
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(homeDestination)
+
+        composeTestRule.runOnUiThread {
+            nestedNavController.navigate(secondNestedDestination)
+        }
+        composeTestRule.waitForIdle()
+
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(homeDestination)
+        assertThat(nestedNavController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(secondNestedDestination)
+
+        composeTestRule.runOnUiThread {
+            navController.navigate(firstSheetDestination)
+        }
+        composeTestRule.waitForIdle()
+
+        assertThat(navigator.sheetState.currentValue)
+            .isAnyOf(ModalBottomSheetValue.HalfExpanded, ModalBottomSheetValue.Expanded)
+
+        composeTestRule.runOnUiThread {
+            backDispatcher.onBackPressed()
+        }
+        composeTestRule.waitForIdle()
+
+        assertThat(navController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(homeDestination)
+        assertThat(nestedNavController.currentBackStackEntry?.destination?.route)
+            .isEqualTo(secondNestedDestination)
+
+        assertThat(navigator.sheetState.currentValue).isEqualTo(ModalBottomSheetValue.Hidden)
+    }
+
+    private fun BottomSheetNavigator.createFakeDestination() =
+        BottomSheetNavigator.Destination(this) {
+            Text("Fake Sheet Content")
+        }
+
+    private val ModalBottomSheetState.isAnimationRunning get() = currentValue != targetValue
+}
diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt b/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt
new file mode 100644
index 0000000..77192b7
--- /dev/null
+++ b/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/NavGraphBuilderTest.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import android.net.Uri
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.core.net.toUri
+import androidx.navigation.NavDeepLinkRequest
+import androidx.navigation.compose.NavHost
+import androidx.navigation.navArgument
+import androidx.navigation.navDeepLink
+import androidx.navigation.plusAssign
+import androidx.navigation.testing.TestNavHostController
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+internal class NavGraphBuilderTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun testCurrentBackStackEntryNavigate() {
+        lateinit var navController: TestNavHostController
+        val key = "key"
+        val arg = "myarg"
+        composeTestRule.setContent {
+            navController = TestNavHostController(LocalContext.current)
+            navController.navigatorProvider += createBottomSheetNavigator()
+
+            NavHost(navController, startDestination = firstRoute) {
+                bottomSheet(firstRoute) { }
+                bottomSheet("$secondRoute/{$key}") { }
+            }
+        }
+
+        composeTestRule.runOnUiThread {
+            navController.navigate("$secondRoute/$arg")
+            assertThat(navController.currentBackStackEntry!!.arguments!!.getString(key))
+                .isEqualTo(arg)
+        }
+    }
+
+    @Test
+    fun testDefaultArguments() {
+        lateinit var navController: TestNavHostController
+        val key = "key"
+        val defaultArg = "default"
+        composeTestRule.setContent {
+            navController = TestNavHostController(LocalContext.current)
+            navController.navigatorProvider += createBottomSheetNavigator()
+
+            NavHost(navController, startDestination = firstRoute) {
+                bottomSheet(firstRoute) { }
+                bottomSheet(
+                    secondRoute,
+                    arguments = listOf(navArgument(key) { defaultValue = defaultArg })
+                ) { }
+            }
+        }
+
+        composeTestRule.runOnUiThread {
+            navController.navigate(secondRoute)
+            assertThat(navController.currentBackStackEntry!!.arguments!!.getString(key))
+                .isEqualTo(defaultArg)
+        }
+    }
+
+    @Test
+    fun testDeepLink() {
+        lateinit var navController: TestNavHostController
+        val uriString = "https://www.example.com"
+        val deeplink = NavDeepLinkRequest.Builder.fromUri(Uri.parse(uriString)).build()
+        composeTestRule.setContent {
+            navController = TestNavHostController(LocalContext.current)
+            navController.navigatorProvider += createBottomSheetNavigator()
+
+            NavHost(navController, startDestination = firstRoute) {
+                bottomSheet(firstRoute) { }
+                bottomSheet(
+                    secondRoute,
+                    deepLinks = listOf(navDeepLink { uriPattern = uriString })
+                ) { }
+            }
+        }
+
+        composeTestRule.runOnUiThread {
+            navController.navigate(uriString.toUri())
+            assertThat(navController.currentBackStackEntry!!.destination.hasDeepLink(deeplink))
+                .isTrue()
+        }
+    }
+
+    private fun createBottomSheetNavigator() =
+        BottomSheetNavigator(sheetState =
+        ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density))
+}
+
+private const val firstRoute = "first"
+private const val secondRoute = "second"
diff --git a/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt b/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt
new file mode 100644
index 0000000..0b03487d
--- /dev/null
+++ b/compose/material/material-navigation/src/androidTest/java/androidx/compose/material/navigation/SheetContentHostTest.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.State
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.testing.TestNavigatorState
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.test.runTest
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+internal class SheetContentHostTest {
+    private val bodyContentTag = "testBodyContent"
+
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun testOnSheetDismissedCalled_ManualDismiss() = runTest {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val backStackEntry = createBackStackEntry(sheetState)
+
+        val dismissedBackStackEntries = mutableListOf<NavBackStackEntry>()
+
+        composeTestRule.setBottomSheetContent(
+            mutableStateOf(backStackEntry),
+            sheetState,
+             },
+             entry -> dismissedBackStackEntries.add(entry) }
+        )
+
+        assertThat(sheetState.currentValue == ModalBottomSheetValue.Expanded).isTrue()
+        composeTestRule.onNodeWithTag(bodyContentTag).performClick()
+        composeTestRule.runOnIdle {
+            assertWithMessage("Sheet is visible")
+                .that(sheetState.isVisible).isFalse()
+            assertWithMessage("Back stack entry should be in the dismissed entries list")
+                .that(dismissedBackStackEntries)
+                .containsExactly(backStackEntry)
+        }
+    }
+
+    @Ignore // b/326117689 to address ModalBottomSheet.show() changing target state to HIDDEN
+    @Test
+    fun testOnSheetDismissedCalled_initiallyExpanded() = runTest {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Expanded, composeTestRule.density)
+        val backStackEntry = createBackStackEntry(sheetState)
+
+        val dismissedBackStackEntries = mutableListOf<NavBackStackEntry>()
+
+        composeTestRule.setBottomSheetContent(
+            mutableStateOf(backStackEntry),
+            sheetState,
+             },
+             entry -> dismissedBackStackEntries.add(entry) }
+        )
+
+        assertThat(sheetState.currentValue == ModalBottomSheetValue.Expanded).isTrue()
+        composeTestRule.onNodeWithTag(bodyContentTag).performClick()
+        composeTestRule.runOnIdle {
+            assertWithMessage("Sheet is not visible")
+                .that(sheetState.isVisible).isFalse()
+            assertWithMessage("Back stack entry should be in the dismissed entries list")
+                .that(dismissedBackStackEntries)
+                .containsExactly(backStackEntry)
+        }
+    }
+
+    @Test
+    fun testOnSheetShownCalled_onBackStackEntryEnter_shortSheet() = runTest {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val backStackEntryState = mutableStateOf<NavBackStackEntry?>(null)
+        val shownBackStackEntries = mutableListOf<NavBackStackEntry>()
+
+        composeTestRule.setBottomSheetContent(
+            backStackEntry = backStackEntryState,
+            sheetState = sheetState,
+             entry -> shownBackStackEntries.add(entry) },
+             }
+        )
+
+        val backStackEntry = createBackStackEntry(sheetState) {
+            Box(Modifier.height(50.dp))
+        }
+        backStackEntryState.value = backStackEntry
+
+        composeTestRule.runOnIdle {
+            assertWithMessage("Sheet is visible")
+                .that(sheetState.isVisible).isTrue()
+            assertWithMessage("Back stack entry should be in the shown entries list")
+                .that(shownBackStackEntries)
+                .containsExactly(backStackEntry)
+        }
+    }
+
+    @Test
+    fun testOnSheetShownCalled_onBackStackEntryEnter_tallSheet() = runTest {
+        val sheetState =
+            ModalBottomSheetState(ModalBottomSheetValue.Hidden, composeTestRule.density)
+        val backStackEntryState = mutableStateOf<NavBackStackEntry?>(null)
+        val shownBackStackEntries = mutableListOf<NavBackStackEntry>()
+
+        composeTestRule.setBottomSheetContent(
+            backStackEntry = backStackEntryState,
+            sheetState = sheetState,
+             entry -> shownBackStackEntries.add(entry) },
+             }
+        )
+
+        val backStackEntry = createBackStackEntry(sheetState) {
+            Box(Modifier.fillMaxSize())
+        }
+        backStackEntryState.value = backStackEntry
+
+        composeTestRule.runOnIdle {
+            assertWithMessage("Sheet is visible")
+                .that(sheetState.isVisible).isTrue()
+            assertWithMessage("Back stack entry should be in the shown entries list")
+                .that(shownBackStackEntries)
+                .containsExactly(backStackEntry)
+        }
+    }
+
+    private fun ComposeContentTestRule.setBottomSheetContent(
+        backStackEntry: State<NavBackStackEntry?>,
+        sheetState: ModalBottomSheetState,
+        onSheetShown: (NavBackStackEntry) -> Unit,
+        onSheetDismissed: (NavBackStackEntry) -> Unit
+    ) {
+        setContent {
+            val saveableStateHolder = rememberSaveableStateHolder()
+            LaunchedEffect(backStackEntry.value) {
+                if (backStackEntry.value == null) sheetState.hide() else sheetState.show()
+            }
+            ModalBottomSheetLayout(
+                sheetContent = {
+                    SheetContentHost(
+                        backStackEntry = backStackEntry.value,
+                        sheetState = sheetState,
+                        saveableStateHolder = saveableStateHolder,
+                        >
+                        >
+                    )
+                },
+                sheetState = sheetState,
+                content = {
+                    Box(
+                        Modifier
+                            .fillMaxSize()
+                            .testTag(bodyContentTag)
+                    )
+                }
+            )
+        }
+    }
+
+    private fun createBackStackEntry(
+        sheetState: ModalBottomSheetState,
+        sheetContent:
+        @Composable ColumnScope.(NavBackStackEntry) -> Unit = { Text("Fake Sheet Content") }
+    ): NavBackStackEntry {
+        val navigatorState = TestNavigatorState()
+        val navigator = BottomSheetNavigator(sheetState)
+        navigator.onAttach(navigatorState)
+
+        val destination = BottomSheetNavigator.Destination(navigator, sheetContent)
+        val backStackEntry = navigatorState.createBackStackEntry(destination, null)
+        navigator.navigate(listOf(backStackEntry), null, null)
+        return backStackEntry
+    }
+}
diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt
new file mode 100644
index 0000000..39ccd43
--- /dev/null
+++ b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheet.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ModalBottomSheetDefaults
+import androidx.compose.material.ModalBottomSheetLayout
+import androidx.compose.material.contentColorFor
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+
+/**
+ * Helper function to create a [ModalBottomSheetLayout] from a [BottomSheetNavigator].
+ *
+ * @see [ModalBottomSheetLayout]
+ */
+@Suppress("MissingJvmstatic")
+@Composable
+// Keep defaults in sync with androidx.compose.material.ModalBottomSheetLayout
+public fun ModalBottomSheetLayout(
+    bottomSheetNavigator: BottomSheetNavigator,
+    modifier: Modifier = Modifier,
+    sheetShape: Shape = MaterialTheme.shapes.large,
+    sheetElevation: Dp = ModalBottomSheetDefaults.Elevation,
+    sheetBackgroundColor: Color = MaterialTheme.colors.surface,
+    sheetContentColor: Color = contentColorFor(sheetBackgroundColor),
+    scrimColor: Color = ModalBottomSheetDefaults.scrimColor,
+    content: @Composable () -> Unit
+) {
+    ModalBottomSheetLayout(
+        sheetState = bottomSheetNavigator.sheetState,
+        sheetContent = bottomSheetNavigator.sheetContent,
+        modifier = modifier,
+        sheetShape = sheetShape,
+        sheetElevation = sheetElevation,
+        sheetBackgroundColor = sheetBackgroundColor,
+        sheetContentColor = sheetContentColor,
+        scrimColor = scrimColor,
+        content = content
+    )
+}
diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt
new file mode 100644
index 0000000..92a2db1
--- /dev/null
+++ b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/BottomSheetNavigator.kt
@@ -0,0 +1,242 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.SpringSpec
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.rememberModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveableStateHolder
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.util.fastForEach
+import androidx.navigation.FloatingWindow
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDestination
+import androidx.navigation.NavOptions
+import androidx.navigation.Navigator
+import androidx.navigation.NavigatorState
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.transform
+
+/**
+ * The state of a [ModalBottomSheetLayout] that the [BottomSheetNavigator] drives
+ *
+ * @param sheetState The sheet state that is driven by the [BottomSheetNavigator]
+ */
+public class BottomSheetNavigatorSheetState(private val sheetState: ModalBottomSheetState) {
+    /**
+     * @see ModalBottomSheetState.isVisible
+     */
+    public val isVisible: Boolean
+        get() = sheetState.isVisible
+
+    /**
+     * @see ModalBottomSheetState.currentValue
+     */
+    public val currentValue: ModalBottomSheetValue
+        get() = sheetState.currentValue
+
+    /**
+     * @see ModalBottomSheetState.targetValue
+     */
+    public val targetValue: ModalBottomSheetValue
+        get() = sheetState.targetValue
+}
+
+/**
+ * Create and remember a [BottomSheetNavigator]
+ */
+@Composable
+public fun rememberBottomSheetNavigator(
+    animationSpec: AnimationSpec<Float> = SpringSpec()
+): BottomSheetNavigator {
+    val sheetState = rememberModalBottomSheetState(
+        ModalBottomSheetValue.Hidden,
+        animationSpec = animationSpec
+    )
+    return remember(sheetState) { BottomSheetNavigator(sheetState) }
+}
+
+/**
+ * Navigator that drives a [ModalBottomSheetState] for use of [ModalBottomSheetLayout]s
+ * with the navigation library. Every destination using this Navigator must set a valid
+ * [Composable] by setting it directly on an instantiated [Destination] or calling
+ * [androidx.compose.material.navigation.bottomSheet].
+ *
+ * <b>The [sheetContent] [Composable] will always host the latest entry of the back stack. When
+ * navigating from a [BottomSheetNavigator.Destination] to another
+ * [BottomSheetNavigator.Destination], the content of the sheet will be replaced instead of a
+ * new bottom sheet being shown.</b>
+ *
+ * When the sheet is dismissed by the user, the [state]'s [NavigatorState.backStack] will be popped.
+ *
+ * The primary constructor is not intended for public use. Please refer to
+ * [rememberBottomSheetNavigator] instead.
+ *
+ * @param sheetState The [ModalBottomSheetState] that the [BottomSheetNavigator] will use to
+ * drive the sheet state
+ */
+@Navigator.Name("bottomSheet")
+public class BottomSheetNavigator(
+    internal val sheetState: ModalBottomSheetState
+) : Navigator<BottomSheetNavigator.Destination>() {
+
+    private var attached by mutableStateOf(false)
+
+    /**
+     * Get the back stack from the [state]. In some cases, the [sheetContent] might be composed
+     * before the Navigator is attached, so we specifically return an empty flow if we aren't
+     * attached yet.
+     */
+    private val backStack: StateFlow<List<NavBackStackEntry>>
+        get() = if (attached) {
+            state.backStack
+        } else {
+            MutableStateFlow(emptyList())
+        }
+
+    /**
+     * Get the transitionsInProgress from the [state]. In some cases, the [sheetContent] might be
+     * composed before the Navigator is attached, so we specifically return an empty flow if we
+     * aren't attached yet.
+     */
+    internal val transitionsInProgress: StateFlow<Set<NavBackStackEntry>>
+        get() = if (attached) {
+            state.transitionsInProgress
+        } else {
+            MutableStateFlow(emptySet())
+        }
+
+    /**
+     * Access properties of the [ModalBottomSheetLayout]'s [ModalBottomSheetState]
+     */
+    public val navigatorSheetState: BottomSheetNavigatorSheetState =
+        BottomSheetNavigatorSheetState(sheetState)
+
+    /**
+     * A [Composable] function that hosts the current sheet content. This should be set as
+     * sheetContent of your [ModalBottomSheetLayout].
+     */
+    internal val sheetContent: @Composable ColumnScope.() -> Unit = {
+        val saveableStateHolder = rememberSaveableStateHolder()
+        val transitionsInProgressEntries by transitionsInProgress.collectAsState()
+
+        // The latest back stack entry, retained until the sheet is completely hidden
+        // While the back stack is updated immediately, we might still be hiding the sheet, so
+        // we keep the entry around until the sheet is hidden
+        val retainedEntry by produceState<NavBackStackEntry?>(
+            initialValue = null,
+            key1 = backStack
+        ) {
+            backStack
+                .transform { backStackEntries ->
+                    // Always hide the sheet when the back stack is updated
+                    // Regardless of whether we're popping or pushing, we always want to hide
+                    // the sheet first before deciding whether to re-show it or keep it hidden
+                    try {
+                        sheetState.hide()
+                    } catch (_: CancellationException) {
+                        // We catch but ignore possible cancellation exceptions as we don't want
+                        // them to bubble up and cancel the whole produceState coroutine
+                    } finally {
+                        emit(backStackEntries.lastOrNull())
+                    }
+                }
+                .collect {
+                    value = it
+                }
+        }
+
+        if (retainedEntry != null) {
+            LaunchedEffect(retainedEntry) {
+                sheetState.show()
+            }
+
+            BackHandler {
+                state.popWithTransition(popUpTo = retainedEntry!!, saveState = false)
+            }
+        }
+
+        SheetContentHost(
+            backStackEntry = retainedEntry,
+            sheetState = sheetState,
+            saveableStateHolder = saveableStateHolder,
+            >
+                transitionsInProgressEntries.forEach(state::markTransitionComplete)
+            },
+             backStackEntry ->
+                // Sheet dismissal can be started through popBackStack in which case we have a
+                // transition that we'll want to complete
+                if (transitionsInProgressEntries.contains(backStackEntry)) {
+                    state.markTransitionComplete(backStackEntry)
+                }
+                // If there is no transition in progress, the sheet has been dimissed by the
+                // user (for example by tapping on the scrim or through an accessibility action)
+                // In this case, we will immediately pop without a transition as the sheet has
+                // already been hidden
+                else {
+                    state.pop(popUpTo = backStackEntry, saveState = false)
+                }
+            }
+        )
+    }
+
+    override fun onAttach(state: NavigatorState) {
+        super.onAttach(state)
+        attached = true
+    }
+
+    override fun createDestination(): Destination = Destination(
+        navigator = this,
+        content = {}
+    )
+
+    override fun navigate(
+        entries: List<NavBackStackEntry>,
+        navOptions: NavOptions?,
+        navigatorExtras: Extras?
+    ) {
+        entries.fastForEach { entry ->
+            state.pushWithTransition(entry)
+        }
+    }
+
+    override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) {
+        state.popWithTransition(popUpTo, savedState)
+    }
+
+    /**
+     * [NavDestination] specific to [BottomSheetNavigator]
+     */
+    @NavDestination.ClassType(Composable::class)
+    public class Destination(
+        navigator: BottomSheetNavigator,
+        internal val content: @Composable ColumnScope.(NavBackStackEntry) -> Unit
+    ) : NavDestination(navigator), FloatingWindow
+}
diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt
new file mode 100644
index 0000000..4f1590a
--- /dev/null
+++ b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/NavGraphBuilder.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.util.fastForEach
+import androidx.navigation.NamedNavArgument
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.NavDeepLink
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.get
+
+/**
+ * Add the [content] [Composable] as bottom sheet content to the [NavGraphBuilder]
+ *
+ * @param route route for the destination
+ * @param arguments list of arguments to associate with destination
+ * @param deepLinks list of deep links to associate with the destinations
+ * @param content the sheet content at the given destination
+ */
+public fun NavGraphBuilder.bottomSheet(
+    route: String,
+    arguments: List<NamedNavArgument> = emptyList(),
+    deepLinks: List<NavDeepLink> = emptyList(),
+    content: @Composable ColumnScope.(backstackEntry: NavBackStackEntry) -> Unit
+) {
+    addDestination(
+        BottomSheetNavigator.Destination(
+            provider[BottomSheetNavigator::class],
+            content
+        ).apply {
+            this.route = route
+            arguments.fastForEach { (argumentName, argument) ->
+                addArgument(argumentName, argument)
+            }
+            deepLinks.fastForEach { deepLink ->
+                addDeepLink(deepLink)
+            }
+        }
+    )
+}
diff --git a/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt
new file mode 100644
index 0000000..c8c3bc7
--- /dev/null
+++ b/compose/material/material-navigation/src/main/java/androidx/compose/material/navigation/SheetContentHost.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 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
+ *
+ *      https://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.material.navigation
+
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.saveable.SaveableStateHolder
+import androidx.compose.runtime.snapshotFlow
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.compose.LocalOwnersProvider
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+
+/**
+ * Hosts a [BottomSheetNavigator.Destination]'s [NavBackStackEntry] and its
+ * [BottomSheetNavigator.Destination.content] and provides a [onSheetDismissed] callback. It also
+ * shows and hides the [ModalBottomSheetLayout] through the [sheetState] when the sheet content
+ * enters or leaves the composition.
+ *
+ * @param backStackEntry The [NavBackStackEntry] holding the [BottomSheetNavigator.Destination],
+ * or null if there is no [NavBackStackEntry]
+ * @param sheetState The [ModalBottomSheetState] used to observe and control the sheet visibility
+ * @param onSheetDismissed Callback when the sheet has been dismissed. Typically, you'll want to
+ * pop the back stack here.
+ */
+@Composable
+internal fun ColumnScope.SheetContentHost(
+    backStackEntry: NavBackStackEntry?,
+    sheetState: ModalBottomSheetState,
+    saveableStateHolder: SaveableStateHolder,
+    onSheetShown: (entry: NavBackStackEntry) -> Unit,
+    onSheetDismissed: (entry: NavBackStackEntry) -> Unit,
+) {
+    if (backStackEntry != null) {
+        val currentOnSheetShown by rememberUpdatedState(onSheetShown)
+        val currentOnSheetDismissed by rememberUpdatedState(onSheetDismissed)
+        LaunchedEffect(sheetState, backStackEntry) {
+            snapshotFlow { sheetState.isVisible }
+                // We are only interested in changes in the sheet's visibility
+                .distinctUntilChanged()
+                // distinctUntilChanged emits the initial value which we don't need
+                .drop(1)
+                .collect { visible ->
+                    if (visible) {
+                        currentOnSheetShown(backStackEntry)
+                    } else {
+                        currentOnSheetDismissed(backStackEntry)
+                    }
+                }
+        }
+        backStackEntry.LocalOwnersProvider(saveableStateHolder) {
+            val content =
+                (backStackEntry.destination as BottomSheetNavigator.Destination).content
+            content(backStackEntry)
+        }
+    }
+}
diff --git a/compose/material/material/integration-tests/material-demos/build.gradle b/compose/material/material/integration-tests/material-demos/build.gradle
index da885ff..c51c55c 100644
--- a/compose/material/material/integration-tests/material-demos/build.gradle
+++ b/compose/material/material/integration-tests/material-demos/build.gradle
@@ -22,6 +22,7 @@
     implementation(project(":compose:integration-tests:demos:common"))
     implementation(project(":compose:material:material"))
     implementation(project(":compose:material:material:material-samples"))
+    implementation project(':compose:material:material-navigation-samples')
     implementation(project(":compose:runtime:runtime"))
     implementation(project(":compose:ui:ui"))
     implementation(project(":compose:ui:ui-text"))
diff --git a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
index a3c0f64..4ca1db4 100644
--- a/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
+++ b/compose/material/material/integration-tests/material-demos/src/main/java/androidx/compose/material/demos/MaterialDemos.kt
@@ -19,6 +19,7 @@
 import androidx.compose.integration.demos.common.ActivityDemo
 import androidx.compose.integration.demos.common.ComposableDemo
 import androidx.compose.integration.demos.common.DemoCategory
+import androidx.compose.material.navigation.samples.BottomSheetNavDemo
 import androidx.compose.material.samples.AlertDialogSample
 import androidx.compose.material.samples.BackdropScaffoldSample
 import androidx.compose.material.samples.BottomDrawerSample
@@ -58,6 +59,7 @@
                     BottomSheetScaffoldWithDrawerSample()
                 },
                 ComposableDemo("Modal Bottom Sheet") { ModalBottomSheetSample() },
+                ComposableDemo("Modal Bottom Sheet In Navigation") { BottomSheetNavDemo() },
             )
         ),
         ComposableDemo("Buttons & FABs") { ButtonDemo() },
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
index b03f6e0..86e9f43 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/ModalBottomSheet.kt
@@ -345,6 +345,7 @@
  */
 @OptIn(ExperimentalMaterialApi::class)
 @Composable
+// Keep defaults in sync with androidx.compose.material.navigation.ModalBottomSheetLayout
 fun ModalBottomSheetLayout(
     sheetContent: @Composable ColumnScope.() -> Unit,
     modifier: Modifier = Modifier,
diff --git a/docs-tip-of-tree/build.gradle b/docs-tip-of-tree/build.gradle
index e2a998c..2022e17 100644
--- a/docs-tip-of-tree/build.gradle
+++ b/docs-tip-of-tree/build.gradle
@@ -98,6 +98,8 @@
     kmpDocs(project(":compose:material:material-icons-core"))
     samples(project(":compose:material:material-icons-core:material-icons-core-samples"))
     kmpDocs(project(":compose:material:material-ripple"))
+    docs(project(":compose:material:material-navigation"))
+    samples(project(":compose:material:material-navigation-samples"))
     samples(project(":compose:material:material:material-samples"))
     kmpDocs(project(":compose:runtime:runtime"))
     samples(project(":compose:runtime:runtime:runtime-samples"))
diff --git a/settings.gradle b/settings.gradle
index b2b275a..e50c120 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -531,6 +531,8 @@
 includeProject(":compose:material:material-ripple", [BuildType.COMPOSE])
 includeProject(":compose:material:material-ripple:material-ripple-benchmark", "compose/material/material-ripple/benchmark", [BuildType.COMPOSE])
 includeProject(":compose:material:material:icons:generator", [BuildType.COMPOSE])
+includeProject(":compose:material:material-navigation", [BuildType.COMPOSE])
+includeProject(":compose:material:material-navigation-samples", "compose/material/material-navigation/samples", [BuildType.COMPOSE])
 includeProject(":compose:material:material:integration-tests:material-demos", [BuildType.COMPOSE])
 includeProject(":compose:material:material:integration-tests:material-catalog", [BuildType.COMPOSE])
 includeProject(":compose:material3:material3:integration-tests:material3-demos", [BuildType.COMPOSE])