Skip to content
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package com.woocommerce.android.ui.bookings.filter

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
Expand All @@ -19,11 +18,15 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.woocommerce.android.R
import com.woocommerce.android.ui.compose.component.Toolbar
import com.woocommerce.android.ui.compose.component.WCColoredButton
Expand All @@ -32,10 +35,6 @@ import com.woocommerce.android.ui.compose.theme.WooThemeWithBackground

@Composable
fun BookingFilterListScreen(state: BookingFilterListUiState) {
BackHandler {
state.onClose()
}

Scaffold(
topBar = {
Column {
Expand Down Expand Up @@ -66,62 +65,94 @@ fun BookingFilterListScreen(state: BookingFilterListUiState) {
},
containerColor = MaterialTheme.colorScheme.surface,
) { innerPadding ->
AnimatedContent(
targetState = state.currentPage,
transitionSpec = {
if (targetState is BookingFilterPage.List) {
slideOut()
} else {
slideIn()
}
},
label = "BookingFiltersAnimatedContent",
FiltersNavHost(
state = state,
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) { page ->
when (page) {
is BookingFilterPage.List -> {
BookingFilterRootPage(state.items)
}
)

BookingFilterPage.AttendanceStatus,
BookingFilterPage.BookingType,
BookingFilterPage.Customer,
BookingFilterPage.Location,
BookingFilterPage.PaymentStatus,
BookingFilterPage.ServiceEvent,
BookingFilterPage.TeamMember,
is BookingFilterPage.DateTime -> {
DateTimeFilterPicker()
}
// The navigation is driven by the state, so we handle back navigation by calling onClose
// We need to ensure that this called after NavHost to make sure we receive back events
BackHandler {
state.onClose()
}
}
}

@Composable
private fun FiltersNavHost(
state: BookingFilterListUiState,
modifier: Modifier
) {
val navController = rememberNavController()

LaunchedEffect(state.currentPage) {
if (state.currentPage != BookingFilterPage.List) {
navController.navigate(state.currentPage.route) {
popUpTo(BookingFilterPage.List.route)
}
} else {
navController.popBackStack(BookingFilterPage.List.route, false)
}
}

NavHost(
navController = navController,
startDestination = BookingFilterPage.List.route,
enterTransition = { slideIn(popNavigation = true) },
exitTransition = { slideOut(popNavigation = true) },
popEnterTransition = { slideIn(popNavigation = false) },
popExitTransition = { slideOut(popNavigation = false) },
modifier = modifier
) {
composable(BookingFilterPage.List.route) {
BookingFilterRootPage(state.items)
}
Comment on lines +109 to +111
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
composable(BookingFilterPage.List.route) {
BookingFilterRootPage(state.items)
}
composable<BookingFilterPage.List> {
BookingFilterRootPage(state.items)
}

np: We could use the type-safe destinations to build the graph. We need to make those serializable, but I don't think it's a problem here.

Copy link
Member Author

@hichamboushaba hichamboushaba Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I thought about it, but given we don't use the serialization plugin on the app, I didn't use, and given we most likely won't need it as the state already exposes all the information that we need, I decided to just use String routes, and they will be static routes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to this, I thought about converting BookingFilterPage to an enum, but I'm not sure if we'll need to pass data for the items in the future, I think given the root ViewModel state is available, we probably won't need any addiitonal data on the currentPage property, WDYT?
If we use an item, we can then just use the name property for route, but it's a minor point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. At the moment, I can't think of any data we would pass so I think we should be okay with an enum!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 6474dc8, I kept using Camel Case for the item names, and kept the route extension, this way even if we need to use a sealed inteface, it will be an easy change.

composable(BookingFilterPage.DateTime.route) {
DateTimeFilterPicker()
}
composable(BookingFilterPage.TeamMember.route) {
TODO()
}
composable(BookingFilterPage.AttendanceStatus.route) {
TODO()
}
composable(BookingFilterPage.PaymentStatus.route) {
TODO()
}
composable(BookingFilterPage.BookingType.route) {
TODO()
}
composable(BookingFilterPage.Customer.route) {
TODO()
}
composable(BookingFilterPage.ServiceEvent.route) {
TODO()
}
composable(BookingFilterPage.Location.route) {
TODO()
}
}
}

private const val TRANSITION_DURATION = 250
private val BookingFilterPage.route: String
get() = this::class.java.simpleName

private fun slideIn(duration: Int = TRANSITION_DURATION): ContentTransform {
return (
slideInHorizontally(animationSpec = tween(durationMillis = duration)) { fullWidth -> fullWidth } +
fadeIn(animationSpec = tween(durationMillis = duration))
) togetherWith (
slideOutHorizontally(animationSpec = tween(durationMillis = duration)) { fullWidth -> -fullWidth } +
fadeOut(animationSpec = tween(durationMillis = duration))
)
private fun slideIn(popNavigation: Boolean): EnterTransition {
return slideInHorizontally(animationSpec = tween(durationMillis = TRANSITION_DURATION)) { fullWidth ->
if (popNavigation) fullWidth else -fullWidth
} + fadeIn(animationSpec = tween(durationMillis = TRANSITION_DURATION))
}

private fun slideOut(duration: Int = TRANSITION_DURATION): ContentTransform {
return (
slideInHorizontally(animationSpec = tween(durationMillis = duration)) { fullWidth -> -fullWidth } +
fadeIn(animationSpec = tween(durationMillis = duration))
) togetherWith (
slideOutHorizontally(animationSpec = tween(durationMillis = duration)) { fullWidth -> fullWidth } +
fadeOut(animationSpec = tween(durationMillis = duration))
)
private fun slideOut(popNavigation: Boolean): ExitTransition {
return slideOutHorizontally(animationSpec = tween(durationMillis = TRANSITION_DURATION)) { fullWidth ->
if (popNavigation) -fullWidth else fullWidth
} + fadeOut(animationSpec = tween(durationMillis = TRANSITION_DURATION))
}

private const val TRANSITION_DURATION = 250

@LightDarkThemePreviews
@Composable
private fun BookingFilterListScreenPreview() {
Expand Down