Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GT-2283 Make the Language Filter SearchBar sticky #3425

Merged
merged 4 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
Expand All @@ -20,6 +21,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
Expand All @@ -32,6 +34,9 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.ccci.gto.android.common.androidx.compose.material3.ui.menu.LazyDropdownMenu
Expand All @@ -43,7 +48,7 @@ import org.cru.godtools.ui.languages.LanguageName
import org.jetbrains.annotations.VisibleForTesting

private val DROPDOWN_MAX_HEIGHT = 700.dp
private val DROPDOWN_MAX_WIDTH = 400.dp
private val DROPDOWN_MAX_WIDTH = 350.dp

internal const val TEST_TAG_FILTER_DROPDOWN = "filter_dropdown"

Expand Down Expand Up @@ -80,7 +85,7 @@ internal fun CategoryFilter(filters: ToolsScreen.Filters, modifier: Modifier = M

ElevatedButton(
onClick = { expanded = !expanded },
modifier = modifier
modifier = modifier.semantics { role = Role.DropdownList }
) {
Text(
selectedCategory?.let { getToolCategoryName(it, LocalContext.current) }
Expand Down Expand Up @@ -134,14 +139,11 @@ internal fun LanguageFilter(filters: ToolsScreen.Filters, modifier: Modifier = M
val selectedLanguage by rememberUpdatedState(filters.selectedLanguage)
val eventSink by rememberUpdatedState(filters.eventSink)

var expanded by rememberSaveable { mutableStateOf(false) }
val expanded by rememberUpdatedState(filters.showLanguagesMenu)

ElevatedButton(
onClick = {
if (!expanded) eventSink(ToolsScreen.FiltersEvent.UpdateLanguageQuery(""))
expanded = !expanded
},
modifier = modifier
onClick = { eventSink(ToolsScreen.FiltersEvent.ToggleLanguagesMenu) },
modifier = modifier.semantics { role = Role.DropdownList }
) {
Text(
text = selectedLanguage?.getDisplayName(context, LocalAppLanguage.current)
Expand All @@ -154,31 +156,35 @@ internal fun LanguageFilter(filters: ToolsScreen.Filters, modifier: Modifier = M

LazyDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier
.sizeIn(maxHeight = DROPDOWN_MAX_HEIGHT, maxWidth = DROPDOWN_MAX_WIDTH)
.testTag(TEST_TAG_FILTER_DROPDOWN)
onDismissRequest = { eventSink(ToolsScreen.FiltersEvent.ToggleLanguagesMenu) },
modifier = Modifier.sizeIn(maxHeight = DROPDOWN_MAX_HEIGHT, maxWidth = DROPDOWN_MAX_WIDTH)
) {
stickyHeader {
Surface(color = MaterialTheme.colorScheme.surface) {
SearchBar(
query,
onQueryChange = { eventSink(ToolsScreen.FiltersEvent.UpdateLanguageQuery(it)) },
onSearch = { eventSink(ToolsScreen.FiltersEvent.UpdateLanguageQuery(it)) },
active = false,
onActiveChange = {},
colors = GodToolsTheme.searchBarColors,
leadingIcon = { Icon(Icons.Filled.Search, null) },
placeholder = {
Text(stringResource(R.string.language_settings_downloadable_languages_search))
},
content = {},
modifier = Modifier
.padding(horizontal = 12.dp)
.fillMaxWidth()
.wrapContentWidth()
)
}
}
item {
SearchBar(
query,
onQueryChange = { eventSink(ToolsScreen.FiltersEvent.UpdateLanguageQuery(it)) },
onSearch = { eventSink(ToolsScreen.FiltersEvent.UpdateLanguageQuery(it)) },
active = false,
onActiveChange = {},
colors = GodToolsTheme.searchBarColors,
leadingIcon = { Icon(Icons.Filled.Search, null) },
placeholder = { Text(stringResource(R.string.language_settings_downloadable_languages_search)) },
content = {},
modifier = Modifier.padding(horizontal = 12.dp)
)
FilterMenuItem(
label = stringResource(R.string.dashboard_tools_section_filter_language_any),
supportingText = stringResource(R.string.dashboard_tools_section_filter_available_tools_all),
onClick = {
eventSink(ToolsScreen.FiltersEvent.SelectLanguage(null))
expanded = false
}
onClick = { eventSink(ToolsScreen.FiltersEvent.SelectLanguage(null)) }
)
}

Expand All @@ -190,10 +196,7 @@ internal fun LanguageFilter(filters: ToolsScreen.Filters, modifier: Modifier = M
count,
count,
),
onClick = {
eventSink(ToolsScreen.FiltersEvent.SelectLanguage(it.code))
expanded = false
},
onClick = { eventSink(ToolsScreen.FiltersEvent.SelectLanguage(it.code)) },
modifier = Modifier.animateItemPlacement()
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import com.slack.circuit.codegen.annotations.CircuitInject
import com.slack.circuit.runtime.Navigator
Expand Down Expand Up @@ -98,7 +99,8 @@ class ToolsPresenter @AssistedInject constructor(

// selected language
val selectedLocale by remember { settings.getDashboardFilterLocaleFlow() }.collectAsState(null)
var languageQuery by remember { mutableStateOf("") }
var showLanguagesMenu by rememberSaveable { mutableStateOf(false) }
var languageQuery by rememberSaveable { mutableStateOf("") }

val filtersEventSink: (ToolsScreen.FiltersEvent) -> Unit = remember {
{
Expand All @@ -108,15 +110,22 @@ class ToolsPresenter @AssistedInject constructor(
}
is ToolsScreen.FiltersEvent.SelectLanguage -> scope.launch {
settings.updateDashboardFilterLocale(it.locale)
showLanguagesMenu = false
languageQuery = ""
}
is ToolsScreen.FiltersEvent.UpdateLanguageQuery -> languageQuery = it.query
ToolsScreen.FiltersEvent.ToggleLanguagesMenu -> {
showLanguagesMenu = !showLanguagesMenu
languageQuery = ""
}
}
}
}

return ToolsScreen.Filters(
categories = rememberFilterCategories(selectedLocale),
selectedCategory = selectedCategory,
showLanguagesMenu = showLanguagesMenu,
languages = rememberFilterLanguages(selectedCategory, languageQuery),
languageQuery = languageQuery,
selectedLanguage = languagesRepository.rememberLanguage(selectedLocale),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data object ToolsScreen : Screen {
data class Filters(
val categories: List<Filter<String>> = emptyList(),
val selectedCategory: String? = null,
val showLanguagesMenu: Boolean = false,
val languages: List<Filter<Language>> = emptyList(),
val languageQuery: String = "",
val selectedLanguage: Language? = null,
Expand All @@ -36,6 +37,7 @@ data object ToolsScreen : Screen {
}

sealed interface FiltersEvent : CircuitUiEvent {
data object ToggleLanguagesMenu : FiltersEvent
data class UpdateLanguageQuery(val query: String) : FiltersEvent
data class SelectCategory(val category: String?) : FiltersEvent
data class SelectLanguage(val locale: Locale?) : FiltersEvent
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.cru.godtools.ui.dashboard.tools

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.ui.Modifier
import com.android.resources.NightMode
import com.google.testing.junit.testparameterinjector.TestParameter
import com.google.testing.junit.testparameterinjector.TestParameterInjector
import java.util.Locale
import kotlin.test.Ignore
import kotlin.test.Test
import org.cru.godtools.model.Language
import org.cru.godtools.ui.BasePaparazziTest
import org.junit.runner.RunWith

@RunWith(TestParameterInjector::class)
class ToolFiltersPaparazziTest(
@TestParameter nightMode: NightMode,
@TestParameter accessibilityMode: AccessibilityMode,
) : BasePaparazziTest(nightMode = nightMode, accessibilityMode = accessibilityMode) {
@Test
fun `LanguageFilter - Button - No Language Selected`() = renderLanguageFilter(
ToolsScreen.Filters(
selectedLanguage = null,
showLanguagesMenu = false,
)
)

@Test
fun `LanguageFilter - Button - English Selected`() = renderLanguageFilter(
ToolsScreen.Filters(
selectedLanguage = Language(Locale.ENGLISH),
showLanguagesMenu = false,
)
)

// TODO: It appears that LayoutLib does not correctly support Popups/Windows currently
// see: https://issuetracker.google.com/issues/317792376
// see: https://issuetracker.google.com/issues/308808808
// see: https://issuetracker.google.com/issues/321623569
@Test
@Ignore("Ignored for now due to LayoutLib rendering issues")
fun `LanguageFilter - Dropdown Menu`() = renderLanguageFilter(
ToolsScreen.Filters(
selectedLanguage = Language(Locale.ENGLISH),
showLanguagesMenu = true,
languages = listOf(
ToolsScreen.Filters.Filter(Language(Locale.ENGLISH), 12345),
ToolsScreen.Filters.Filter(Language(Locale.FRENCH), 1),
ToolsScreen.Filters.Filter(Language(Locale("es")), 3),
),
)
)

private fun renderLanguageFilter(filters: ToolsScreen.Filters) = centerInSnapshot {
LanguageFilter(filters, modifier = Modifier.fillMaxWidth(0.5f))
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.cru.godtools.ui.dashboard.tools

import android.app.Application
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.hasClickAction
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
Expand All @@ -14,7 +15,6 @@ import org.cru.godtools.model.Tool
import org.cru.godtools.ui.dashboard.tools.ToolsScreen.Filters.Filter
import org.junit.Rule
import org.junit.Test
import org.junit.experimental.categories.Categories.CategoryFilter
import org.junit.runner.RunWith
import org.robolectric.annotation.Config

Expand Down Expand Up @@ -167,25 +167,27 @@ class ToolFiltersTest {
}

@Test
fun `LanguageFilter() - Dropdown Menu - Show when button is clicked`() {
fun `LanguageFilter() - Button toggles menu`() {
composeTestRule.setContent {
LanguageFilter(ToolsScreen.Filters(eventSink = events))
LanguageFilter(
ToolsScreen.Filters(
selectedLanguage = Language(Locale.ENGLISH),
eventSink = events,
)
)
}

// dropdown menu not shown
composeTestRule.onNodeWithTag(TEST_TAG_FILTER_DROPDOWN).assertDoesNotExist()

// click button to show dropdown
composeTestRule.onNode(hasClickAction()).performClick()
composeTestRule.onNodeWithTag(TEST_TAG_FILTER_DROPDOWN).assertExists()
events.assertEvent(ToolsScreen.FiltersEvent.UpdateLanguageQuery(""))
composeTestRule.onNodeWithText("English").assertHasClickAction().performClick()
events.assertEvent(ToolsScreen.FiltersEvent.ToggleLanguagesMenu)
}

@Test
fun `LanguageFilter() - Dropdown Menu - Show languages`() {
composeTestRule.setContent {
LanguageFilter(
filters = ToolsScreen.Filters(
showLanguagesMenu = true,
languages = listOf(
Filter(Language(Locale.FRENCH), 1),
Filter(Language(Locale.GERMAN), 1),
Expand All @@ -194,12 +196,10 @@ class ToolFiltersTest {
),
)
}
composeTestRule.onNode(hasClickAction()).performClick()

composeTestRule.onNodeWithText("English", substring = true, ignoreCase = true).assertDoesNotExist()
composeTestRule.onNodeWithText("French", substring = true, ignoreCase = true).assertExists()
composeTestRule.onNodeWithText("German", substring = true, ignoreCase = true).assertExists()
events.assertEvent(ToolsScreen.FiltersEvent.UpdateLanguageQuery(""))
}

@Test
Expand All @@ -208,6 +208,7 @@ class ToolFiltersTest {
LanguageFilter(
filters = ToolsScreen.Filters(
selectedLanguage = Language(Locale.FRENCH),
showLanguagesMenu = true,
languages = listOf(
Filter(Language(Locale.FRENCH), 1),
Filter(Language(Locale.GERMAN), 1),
Expand All @@ -216,21 +217,17 @@ class ToolFiltersTest {
),
)
}
composeTestRule.onNode(hasClickAction()).performClick()

composeTestRule.onNodeWithText("Any language", substring = true, ignoreCase = true).performClick()
composeTestRule.onNodeWithTag(TEST_TAG_FILTER_DROPDOWN).assertDoesNotExist()
events.assertEvents(
ToolsScreen.FiltersEvent.UpdateLanguageQuery(""),
ToolsScreen.FiltersEvent.SelectLanguage(null)
)
events.assertEvent(ToolsScreen.FiltersEvent.SelectLanguage(null))
}

@Test
fun `LanguageFilter() - Dropdown Menu - Select a language`() {
composeTestRule.setContent {
LanguageFilter(
filters = ToolsScreen.Filters(
showLanguagesMenu = true,
languages = listOf(
Filter(Language(Locale.FRENCH), 1),
Filter(Language(Locale.GERMAN), 1),
Expand All @@ -239,14 +236,9 @@ class ToolFiltersTest {
),
)
}
composeTestRule.onNode(hasClickAction()).performClick()

composeTestRule.onNodeWithText("French", substring = true, ignoreCase = true).performClick()
composeTestRule.onNodeWithTag(TEST_TAG_FILTER_DROPDOWN).assertDoesNotExist()
events.assertEvents(
ToolsScreen.FiltersEvent.UpdateLanguageQuery(""),
ToolsScreen.FiltersEvent.SelectLanguage(Locale.FRENCH)
)
events.assertEvents(ToolsScreen.FiltersEvent.SelectLanguage(Locale.FRENCH))
}
// endregion LanguageFilter
}
Loading