Bug 1864076 - Integrate Navigation Middleware into the Debug Drawer

fenix/123.0
Noah Bond 5 months ago committed by mergify[bot]
parent 5e75e68d13
commit 0613321c15

@ -10,12 +10,15 @@ import androidx.compose.runtime.Composable
/**
* A navigation destination for screens within the Debug Drawer.
*
* @property route The navigation route of the destination.
* @property route The unique route used to navigate to the destination. This string can also contain
* optional parameters for arguments or deep linking.
* @property title The string ID of the destination's title.
* @property onClick Invoked when the destination is clicked to be navigated to.
* @property content The destination's [Composable].
*/
data class DebugDrawerDestination(
val route: String,
@StringRes val title: Int,
val onClick: () -> Unit,
val content: @Composable () -> Unit,
)

@ -5,25 +5,22 @@
package org.mozilla.fenix.debugsettings.navigation
import androidx.annotation.StringRes
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import org.mozilla.fenix.R
import org.mozilla.fenix.debugsettings.ui.DebugDrawerHome
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.debugsettings.tabs.TabTools as TabToolsScreen
/**
* The navigation routes for screens within the Debug Drawer.
*
* @property route The navigation route of the destination.
* @property route The unique route used to navigate to the destination. This string can also contain
* optional parameters for arguments or deep linking.
* @property title The string ID of the destination's title.
*/
enum class DebugDrawerRoute(val route: String, @StringRes val title: Int) {
/**
* The navigation route for [DebugDrawerHome].
*/
Home(
route = "home",
title = R.string.debug_drawer_title,
),
/**
* The navigation route for [TabToolsScreen].
*/
@ -31,22 +28,41 @@ enum class DebugDrawerRoute(val route: String, @StringRes val title: Int) {
route = "tab_tools",
title = R.string.debug_drawer_tab_tools_title,
),
}
;
/**
* Creates a list of [DebugDrawerDestination]s for the [DebugDrawerRoute]s in Fenix.
*/
fun debugDrawerDestinationsFromRoutes(
// Composable content dependencies will go here (e.g. browser store, the list of menu items, etc.)
): List<DebugDrawerDestination> = DebugDrawerRoute.values().map { debugDrawerRoute ->
DebugDrawerDestination(
route = debugDrawerRoute.route,
title = debugDrawerRoute.title,
content = {
when (debugDrawerRoute) {
DebugDrawerRoute.Home -> {}
DebugDrawerRoute.TabTools -> {}
companion object {
/**
* Transforms the values of [DebugDrawerRoute] into a list of [DebugDrawerDestination]s.
*
* @param debugDrawerStore [DebugDrawerStore] used to dispatch navigation actions.
*/
fun generateDebugDrawerDestinations(
debugDrawerStore: DebugDrawerStore,
): List<DebugDrawerDestination> =
DebugDrawerRoute.values().map { debugDrawerRoute ->
val onClick: () -> Unit
val content: @Composable () -> Unit
when (debugDrawerRoute) {
TabTools -> {
onClick = {
debugDrawerStore.dispatch(DebugDrawerAction.NavigateTo.TabTools)
}
content = {
Text(
text = "Tab tools",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
}
}
}
DebugDrawerDestination(
route = debugDrawerRoute.route,
title = debugDrawerRoute.title,
onClick = onClick,
content = content,
)
}
},
)
}
}

@ -5,17 +5,22 @@
package org.mozilla.fenix.debugsettings.store
import androidx.navigation.NavHostController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.ui.DEBUG_DRAWER_HOME_ROUTE
/**
* Middleware that handles navigation events for the Debug Drawer feature.
*
* @param navController [NavHostController] used to execute any navigation actions on the UI.
* @param scope [CoroutineScope] used to make calls to the main thread.
*/
class DebugDrawerNavigationMiddleware(
private val navController: NavHostController,
private val scope: CoroutineScope,
) : Middleware<DebugDrawerState, DebugDrawerAction> {
override fun invoke(
@ -23,11 +28,18 @@ class DebugDrawerNavigationMiddleware(
next: (DebugDrawerAction) -> Unit,
action: DebugDrawerAction,
) {
when (action) {
is DebugDrawerAction.NavigateTo.Home -> navController.navigate(route = DebugDrawerRoute.Home.route)
is DebugDrawerAction.NavigateTo.TabTools -> navController.navigate(route = DebugDrawerRoute.TabTools.route)
is DebugDrawerAction.OnBackPressed -> navController.popBackStack()
is DebugDrawerAction.DrawerOpened, DebugDrawerAction.DrawerClosed -> {} // no-op
next(action)
scope.launch {
when (action) {
is DebugDrawerAction.NavigateTo.Home -> navController.popBackStack(
route = DEBUG_DRAWER_HOME_ROUTE,
inclusive = false,
)
is DebugDrawerAction.NavigateTo.TabTools ->
navController.navigate(route = DebugDrawerRoute.TabTools.route)
is DebugDrawerAction.OnBackPressed -> navController.popBackStack()
is DebugDrawerAction.DrawerOpened, DebugDrawerAction.DrawerClosed -> Unit // no-op
}
}
}
}

@ -27,32 +27,26 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The debug drawer UI.
*
* @param navController [NavHostController] used to perform navigation actions on the [NavHost].
* @param destinations The list of [DebugDrawerDestination]s (excluding home) used to populate
* the [NavHost] with screens.
* @param onBackButtonClick Invoked when the user taps on the back button in the app bar.
*/
@Composable
fun DebugDrawer(
navController: NavHostController,
destinations: List<DebugDrawerDestination>,
onBackButtonClick: () -> Unit,
) {
var backButtonVisible by remember { mutableStateOf(false) }
var toolbarTitle by remember { mutableStateOf("") }
// This is temporary until https://bugzilla.mozilla.org/show_bug.cgi?id=1864076
val homeMenuItems = List(size = 5) {
DebugDrawerMenuItem(
label = "Screen $it",
onClick = {
navController.navigate("screen_$it")
},
)
}
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(
title = {
@ -73,25 +67,21 @@ fun DebugDrawer(
NavHost(
navController = navController,
startDestination = "home",
startDestination = DEBUG_DRAWER_HOME_ROUTE,
modifier = Modifier.fillMaxSize(),
) {
composable(route = "home") {
composable(route = DEBUG_DRAWER_HOME_ROUTE) {
toolbarTitle = stringResource(id = R.string.debug_drawer_title)
backButtonVisible = false
DebugDrawerHome(menuItems = homeMenuItems)
DebugDrawerHome(destinations = destinations)
}
homeMenuItems.forEachIndexed { index, item ->
composable(route = "screen_$index") {
toolbarTitle = item.label
destinations.forEach { destination ->
composable(route = destination.route) {
toolbarTitle = stringResource(id = destination.title)
backButtonVisible = true
Text(
text = "Screen $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
destination.content()
}
}
}
@ -115,11 +105,30 @@ private fun topBarBackButton(onClick: () -> Unit): @Composable () -> Unit = {
@LightDarkPreview
private fun DebugDrawerPreview() {
val navController = rememberNavController()
val destinations = remember {
List(size = 15) { index ->
DebugDrawerDestination(
route = "screen_$index",
title = R.string.debug_drawer_title,
onClick = {
navController.navigate(route = "screen_$index")
},
content = {
Text(
text = "Tool $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
)
}
}
FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
DebugDrawer(
navController = navController,
destinations = destinations,
onBackButtonClick = {
navController.popBackStack()
},

@ -24,24 +24,32 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.appVersionName
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.compose.list.TextListItem
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The navigation route for [DebugDrawerHome].
*/
const val DEBUG_DRAWER_HOME_ROUTE = "debug_drawer_home"
/**
* The home screen of the [DebugDrawer].
*
* @param menuItems The list of [DebugDrawerMenuItem]s.
* @param destinations The list of [DebugDrawerDestination]s to display.
*/
@Composable
fun DebugDrawerHome(
menuItems: List<DebugDrawerMenuItem>,
destinations: List<DebugDrawerDestination>,
) {
val lazyListState = rememberLazyListState()
@ -85,14 +93,14 @@ fun DebugDrawerHome(
}
items(
items = menuItems,
key = { menuItem ->
menuItem.label
items = destinations,
key = { destination ->
destination.route
},
) { menuItem ->
) { destination ->
TextListItem(
label = menuItem.label,
onClick = menuItem.onClick,
label = stringResource(id = destination.title),
onClick = destination.onClick,
)
Divider()
@ -102,21 +110,23 @@ fun DebugDrawerHome(
@Composable
@LightDarkPreview
private fun DebugDrawerPreview() {
private fun DebugDrawerHomePreview() {
val scope = rememberCoroutineScope()
val snackbarState = remember { SnackbarHostState() }
FirefoxTheme {
Box {
DebugDrawerHome(
menuItems = List(size = 30) {
DebugDrawerMenuItem(
label = "Navigation $it",
destinations = List(size = 30) {
DebugDrawerDestination(
route = "screen_$it",
title = R.string.debug_drawer_title,
onClick = {
scope.launch {
snackbarState.showSnackbar("$it clicked")
snackbarState.showSnackbar("item $it clicked")
}
},
content = {},
)
},
)

@ -1,16 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.debugsettings.ui
/**
* Menu items to be presented within [DebugDrawer].
*
* @property label The label of the menu item.
* @property onClick Invoked when the menu item is clicked.
*/
data class DebugDrawerMenuItem(
val label: String,
val onClick: () -> Unit,
)

@ -12,6 +12,7 @@ import androidx.compose.material.ModalDrawer
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@ -34,6 +35,7 @@ import kotlinx.coroutines.flow.filter
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.FloatingActionButton
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.debugsettings.store.DrawerStatus
import org.mozilla.fenix.theme.FirefoxTheme
@ -42,17 +44,20 @@ import org.mozilla.fenix.theme.FirefoxTheme
*
* @param navController [NavHostController] used to perform navigation actions.
* @param drawerStatus The [DrawerStatus] indicating the physical state of the drawer.
* @param debugDrawerDestinations The complete list of [DebugDrawerDestination]s used to populate
* the [DebugDrawer] with sub screens.
* @param onDrawerOpen Invoked when the drawer is opened.
* @param onDrawerClose Invoked when the drawer is closed.
* @param onBackButtonClick Invoked when the user taps on the back button in the app bar.
* @param onDrawerBackButtonClick Invoked when the user taps on the back button in the app bar.
*/
@Composable
fun DebugOverlay(
navController: NavHostController,
drawerStatus: DrawerStatus,
debugDrawerDestinations: List<DebugDrawerDestination>,
onDrawerOpen: () -> Unit,
onDrawerClose: () -> Unit,
onBackButtonClick: () -> Unit,
onDrawerBackButtonClick: () -> Unit,
) {
val snackbarState = remember { SnackbarHostState() }
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
@ -103,7 +108,8 @@ fun DebugOverlay(
CompositionLocalProvider(LocalLayoutDirection provides currentLayoutDirection) {
DebugDrawer(
navController = navController,
onBackButtonClick = onBackButtonClick,
destinations = debugDrawerDestinations,
onBackButtonClick = onDrawerBackButtonClick,
)
}
},
@ -132,18 +138,37 @@ fun DebugOverlay(
private fun DebugOverlayPreview() {
val navController = rememberNavController()
var drawerStatus by remember { mutableStateOf(DrawerStatus.Closed) }
val destinations = remember {
List(size = 15) { index ->
DebugDrawerDestination(
route = "screen_$index",
title = R.string.debug_drawer_title,
onClick = {
navController.navigate(route = "screen_$index")
},
content = {
Text(
text = "Tool $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
)
}
}
FirefoxTheme {
DebugOverlay(
navController = navController,
drawerStatus = drawerStatus,
debugDrawerDestinations = destinations,
onDrawerOpen = {
drawerStatus = DrawerStatus.Open
},
onDrawerClose = {
drawerStatus = DrawerStatus.Closed
},
onBackButtonClick = {
onDrawerBackButtonClick = {
navController.popBackStack()
},
)

@ -7,10 +7,13 @@ package org.mozilla.fenix.debugsettings.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.compose.rememberNavController
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerNavigationMiddleware
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.debugsettings.store.DrawerStatus
import org.mozilla.fenix.theme.FirefoxTheme
@ -22,7 +25,22 @@ import org.mozilla.fenix.theme.Theme
@Composable
fun FenixOverlay() {
val navController = rememberNavController()
val debugDrawerStore = remember { DebugDrawerStore() }
val coroutineScope = rememberCoroutineScope()
val debugDrawerStore = remember {
DebugDrawerStore(
middlewares = listOf(
DebugDrawerNavigationMiddleware(
navController = navController,
scope = coroutineScope,
),
),
)
}
val debugDrawerDestinations = remember {
DebugDrawerRoute.generateDebugDrawerDestinations(
debugDrawerStore = debugDrawerStore,
)
}
val drawerStatus by debugDrawerStore.observeAsState(initialValue = DrawerStatus.Closed) { state ->
state.drawerStatus
}
@ -31,13 +49,16 @@ fun FenixOverlay() {
DebugOverlay(
navController = navController,
drawerStatus = drawerStatus,
debugDrawerDestinations = debugDrawerDestinations,
onDrawerOpen = {
debugDrawerStore.dispatch(DebugDrawerAction.DrawerOpened)
},
onDrawerClose = {
debugDrawerStore.dispatch(DebugDrawerAction.DrawerClosed)
},
onBackButtonClick = {},
onDrawerBackButtonClick = {
debugDrawerStore.dispatch(DebugDrawerAction.OnBackPressed)
},
)
}
}

@ -9,15 +9,22 @@ import io.mockk.called
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerNavigationMiddleware
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.debugsettings.ui.DEBUG_DRAWER_HOME_ROUTE
class DebugDrawerNavigationMiddlewareTest {
@get:Rule
val coroutinesTestRule = MainCoroutineRule()
private val testCoroutineScope = coroutinesTestRule.scope
private val navController: NavHostController = mockk(relaxed = true)
private lateinit var store: DebugDrawerStore
@ -27,16 +34,17 @@ class DebugDrawerNavigationMiddlewareTest {
middlewares = listOf(
DebugDrawerNavigationMiddleware(
navController = navController,
scope = testCoroutineScope,
),
),
)
}
@Test
fun `WHEN home is the next destination THEN home is navigated to`() {
fun `WHEN home is the next destination THEN the back stack is cleared and the user is returned to home`() {
store.dispatch(DebugDrawerAction.NavigateTo.Home).joinBlocking()
verify { navController.navigate(DebugDrawerRoute.Home.route) }
verify { navController.popBackStack(route = DEBUG_DRAWER_HOME_ROUTE, inclusive = false) }
}
@Test

Loading…
Cancel
Save