From eb280a73460d1af23baeefc36b3cea5d34b4dc84 Mon Sep 17 00:00:00 2001 From: Pablo Pajuelo Cabezas Date: Tue, 19 Nov 2024 16:08:06 +0100 Subject: [PATCH] hotfix: async fetching of dropdown items Signed-off-by: Pablo Pajuelo Cabezas --- .../kotlin/org/hisp/dhis/common/App.kt | 25 ++- .../components/GroupComponentDropDown.kt | 13 +- .../parameter/ParameterSelectorScreen.kt | 14 +- .../toggleableInputs/InputDropDownScreen.kt | 97 ++++++++---- .../designsystem/component/InputDropDown.kt | 142 ++++++++++-------- 5 files changed, 186 insertions(+), 105 deletions(-) diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt index e8ad47b1a..c1cd7ee39 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/App.kt @@ -78,9 +78,8 @@ fun Main( modifier = Modifier .background(SurfaceColor.Container), ) { - val screenDropdownItemList = mutableListOf() - Groups.entries.forEach { - screenDropdownItemList.add(DropdownItem(it.label)) + var screenDropdownItemList by remember { + mutableStateOf(emptyList()) } if (isComponentSelected) { @@ -91,14 +90,30 @@ fun Main( top = Spacing.Spacing16, ), title = "Group", - dropdownItems = screenDropdownItemList.toList(), - onItemSelected = { currentScreen.value = getCurrentScreen(it.label) }, + fetchItem = { index -> screenDropdownItemList[index] }, + itemCount = screenDropdownItemList.size, + onSearchOption = { query -> + screenDropdownItemList = if (query.isNotEmpty()) { + screenDropdownItemList.filter { it.label.contains(query) } + } else { + Groups.entries.map { + DropdownItem(it.label) + } + } + }, + useDropDown = false, + onItemSelected = { _, item -> currentScreen.value = getCurrentScreen(item.label) }, onResetButtonClicked = { currentScreen.value = Groups.NO_GROUP_SELECTED }, state = InputShellState.UNFOCUSED, expanded = true, selectedItem = DropdownItem(currentScreen.value.label), inputStyle = InputStyle.DataInputStyle() .apply { backGroundColor = SurfaceColor.SurfaceBright }, + loadOptions = { + screenDropdownItemList = Groups.entries.map { + DropdownItem(it.label) + } + }, ) when (currentScreen.value) { diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/components/GroupComponentDropDown.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/components/GroupComponentDropDown.kt index 66bb7cba0..480b59d88 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/components/GroupComponentDropDown.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/components/GroupComponentDropDown.kt @@ -21,11 +21,18 @@ fun GroupComponentDropDown( InputDropDown( modifier = modifier.padding(horizontal = Spacing.Spacing16), title = "Component", - dropdownItems = dropdownItems, - onItemSelected = onItemSelected, + itemCount = dropdownItems.size, + onSearchOption = {}, + fetchItem = { index -> dropdownItems[index] }, + onItemSelected = { _, item -> onItemSelected(item) }, onResetButtonClicked = onResetButtonClicked, selectedItem = selectedItem, state = InputShellState.UNFOCUSED, - inputStyle = InputStyle.DataInputStyle().apply { backGroundColor = SurfaceColor.SurfaceBright }, + useDropDown = false, + inputStyle = InputStyle.DataInputStyle() + .apply { backGroundColor = SurfaceColor.SurfaceBright }, + loadOptions = { + /*no-op*/ + }, ) } diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt index 224075a9e..c834aadca 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/parameter/ParameterSelectorScreen.kt @@ -233,12 +233,16 @@ fun ParameterSelectorScreen() { title = "DropDown parameter", state = InputShellState.UNFOCUSED, inputStyle = InputStyle.ParameterInputStyle(), - dropdownItems = listOf( - DropdownItem("Item 1"), - DropdownItem("Item 2"), - ), - onItemSelected = {}, + itemCount = 2, + onSearchOption = {}, + fetchItem = { index -> + DropdownItem("Item $index") + }, + onItemSelected = { _, _ -> }, onResetButtonClicked = {}, + loadOptions = { + /*no-op*/ + }, ) }, onExpand = {}, diff --git a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt index 099ccfa21..acdcc2c64 100644 --- a/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt +++ b/common/src/commonMain/kotlin/org/hisp/dhis/common/screens/toggleableInputs/InputDropDownScreen.kt @@ -30,64 +30,85 @@ fun InputDropDownScreen() { ) var selectedItem by remember { mutableStateOf(null) } - ColumnComponentContainer("Basic Input Dropdown with < 7 inputs") { + ColumnComponentContainer("Basic Input Dropdown") { InputDropDown( title = "Label", state = InputShellState.UNFOCUSED, - dropdownItems = options.take(6), + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, onResetButtonClicked = { selectedItem = null }, - onItemSelected = { - selectedItem = it + onItemSelected = { _, item -> + selectedItem = item }, selectedItem = selectedItem, + loadOptions = { + /*no-op*/ + }, ) InputDropDown( title = "Label - With supporting text", state = InputShellState.UNFOCUSED, - dropdownItems = options.take(6), + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, onResetButtonClicked = { selectedItem = null }, - onItemSelected = { - selectedItem = it + onItemSelected = { _, item -> + selectedItem = item }, selectedItem = selectedItem, supportingTextData = listOf( SupportingTextData(text = "Options"), ), + loadOptions = { + /*no-op*/ + }, ) InputDropDown( title = "Label - Parameter Style", inputStyle = InputStyle.ParameterInputStyle(), state = InputShellState.UNFOCUSED, - dropdownItems = options.take(6), + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, onResetButtonClicked = { selectedItem = null }, - onItemSelected = { - selectedItem = it + onItemSelected = { _, item -> + selectedItem = item }, selectedItem = selectedItem, + loadOptions = { + /*no-op*/ + }, ) } - ColumnComponentContainer("Basic Input Dropdown with >= 7 inputs") { + ColumnComponentContainer("Basic Input Dropdown for large set") { var selectedItem4 by remember { mutableStateOf(null) } InputDropDown( title = "Label", state = InputShellState.UNFOCUSED, - dropdownItems = options, + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, + useDropDown = false, onResetButtonClicked = { selectedItem4 = null }, - onItemSelected = { - selectedItem4 = it + onItemSelected = { _, item -> + selectedItem4 = item }, selectedItem = selectedItem4, + loadOptions = { + /*no-op*/ + }, ) } @@ -96,14 +117,20 @@ fun InputDropDownScreen() { InputDropDown( title = "Label", state = InputShellState.UNFOCUSED, - dropdownItems = options, + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, + useDropDown = false, onResetButtonClicked = { selectedItem1 = null }, - onItemSelected = { - selectedItem1 = it + onItemSelected = { _, item -> + selectedItem1 = item }, selectedItem = selectedItem1, + loadOptions = { + /*no-op*/ + }, ) } @@ -112,14 +139,20 @@ fun InputDropDownScreen() { InputDropDown( title = "Label", state = InputShellState.ERROR, - dropdownItems = options, + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, + useDropDown = false, onResetButtonClicked = { selectedItem2 = null }, - onItemSelected = { - selectedItem2 = it + onItemSelected = { _, item -> + selectedItem2 = item }, selectedItem = selectedItem2, + loadOptions = { + /*no-op*/ + }, ) } @@ -128,14 +161,20 @@ fun InputDropDownScreen() { InputDropDown( title = "Label", state = InputShellState.DISABLED, - dropdownItems = options, + itemCount = options.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, + useDropDown = false, onResetButtonClicked = { selectedItem3 = null }, - onItemSelected = { - selectedItem3 = it + onItemSelected = { _, item -> + selectedItem3 = item }, selectedItem = selectedItem3, + loadOptions = { + /*no-op*/ + }, ) } @@ -148,14 +187,20 @@ fun InputDropDownScreen() { InputDropDown( title = "Label", state = InputShellState.UNFOCUSED, - dropdownItems = dropdownItems, + itemCount = dropdownItems.size, + onSearchOption = {}, + fetchItem = { index -> options[index] }, + useDropDown = false, onResetButtonClicked = { selectedItem = null }, - onItemSelected = { - selectedItem = it + onItemSelected = { _, item -> + selectedItem = item }, selectedItem = selectedItem, + loadOptions = { + /*no-op*/ + }, ) } } diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt index 089621295..44dfb0d42 100644 --- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt +++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/InputDropDown.kt @@ -4,15 +4,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Cancel -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults @@ -76,7 +76,9 @@ fun InputDropDown( title: String, state: InputShellState, inputStyle: InputStyle = InputStyle.DataInputStyle(), - dropdownItems: List, + itemCount: Int, + onSearchOption: (String) -> Unit, + fetchItem: (index: Int) -> DropdownItem, selectedItem: DropdownItem? = null, supportingTextData: List? = null, legendData: LegendData? = null, @@ -84,14 +86,17 @@ fun InputDropDown( modifier: Modifier = Modifier, onFocusChanged: ((Boolean) -> Unit)? = null, onResetButtonClicked: () -> Unit, - onItemSelected: (DropdownItem) -> Unit, + onItemSelected: (index: Int, item: DropdownItem) -> Unit, showSearchBar: Boolean = true, expanded: Boolean = false, + useDropDown: Boolean = true, + loadOptions: () -> Unit, noResultsFoundString: String = provideStringResource("no_results_found"), searchToFindMoreString: String = provideStringResource("search_to_see_more"), ) { val focusRequester = remember { FocusRequester() } var showDropdown by remember { mutableStateOf(expanded) } + var currentItem by remember(selectedItem) { mutableStateOf(selectedItem) } val inputField: @Composable (modifier: Modifier) -> Unit = { inputModifier -> DropdownInputField( @@ -105,7 +110,7 @@ fun InputDropDown( onFocusChanged = onFocusChanged, supportingTextData = supportingTextData, legendData = legendData, - selectedItem = selectedItem, + selectedItem = currentItem, onResetButtonClicked = onResetButtonClicked, onDropdownIconClick = { showDropdown = !showDropdown @@ -113,61 +118,60 @@ fun InputDropDown( ) } - if (dropdownItems.size > MAX_DROPDOWN_ITEMS) { + if (!useDropDown) { Box { inputField(modifier) if (showDropdown) { var searchQuery by remember { mutableStateOf("") } - var filteredOptions = dropdownItems - - if (searchQuery.isNotEmpty()) { - filteredOptions = - dropdownItems.filter { it.label.contains(searchQuery, ignoreCase = true) } - } - + val scrollState = rememberLazyListState() BottomSheetShell( modifier = Modifier.testTag("INPUT_DROPDOWN_BOTTOM_SHEET"), title = title, + contentScrollState = scrollState, content = { - Column( + LazyColumn( modifier = Modifier .testTag("INPUT_DROPDOWN_BOTTOM_SHEET_ITEMS") .padding(top = Spacing8), + state = scrollState, ) { - if (filteredOptions.isNotEmpty()) { - filteredOptions - .take(MAX_DROPDOWN_ITEMS_TO_SHOW) - .forEachIndexed { index, item -> - DropdownItem( - modifier = Modifier.testTag("INPUT_DROPDOWN_BOTTOM_SHEET_ITEM_$index"), - item = item, - selected = selectedItem == item, - contentPadding = PaddingValues(Spacing8), - onItemClick = { - onItemSelected(item) - showDropdown = false - }, - ) + when { + itemCount > 0 -> + items(count = itemCount) { index -> + with(fetchItem(index)) { + DropdownItem( + modifier = Modifier.testTag("INPUT_DROPDOWN_BOTTOM_SHEET_ITEM_$index"), + item = this, + selected = selectedItem == this, + contentPadding = PaddingValues(Spacing8), + onItemClick = { + currentItem = this + onItemSelected(index, this) + showDropdown = false + }, + ) + } } - if (filteredOptions.size > MAX_DROPDOWN_ITEMS_TO_SHOW) { - Text( - text = searchToFindMoreString, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - ) + + searchQuery.isEmpty() -> { + item { + ProgressIndicator(type = ProgressIndicatorType.CIRCULAR_SMALL) + } + loadOptions() } - } else { - Text( - text = noResultsFoundString, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - ) + + else -> + item { + Text( + text = noResultsFoundString, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + ) + } } } }, @@ -179,8 +183,14 @@ fun InputDropDown( } else { null }, - onSearch = { searchQuery = it }, - onSearchQueryChanged = { searchQuery = it }, + onSearch = { + searchQuery = it + onSearchOption(it) + }, + onSearchQueryChanged = { + searchQuery = it + onSearchOption(it) + }, ) } } @@ -199,10 +209,7 @@ fun InputDropDown( MaterialTheme( shapes = Shapes(extraSmall = RoundedCornerShape(Spacing8)), ) { - // TODO: Replace with ExposedDropdownMenu once the fix for the following issue - // is available in Compose Multiplatform - // https://issuetracker.google.com/issues/205589613 - DropdownMenu( + ExposedDropdownMenu( expanded = showDropdown, onDismissRequest = { showDropdown = false }, modifier = Modifier.background( @@ -210,22 +217,25 @@ fun InputDropDown( shape = RoundedCornerShape(Spacing8), ).exposedDropdownSize().testTag("INPUT_DROPDOWN_MENU"), ) { - dropdownItems.forEachIndexed { index, item -> - DropdownItem( - modifier = Modifier.testTag("INPUT_DROPDOWN_MENU_ITEM_$index") - .fillMaxWidth() - .padding(start = dropdownStartPadding(inputStyle) + 8.dp), - item = item, - selected = selectedItem == item, - contentPadding = PaddingValues( - horizontal = Spacing8, - vertical = Spacing16, - ), - onItemClick = { - onItemSelected(item) - showDropdown = false - }, - ) + repeat(itemCount) { index -> + with(fetchItem(index)) { + DropdownItem( + modifier = Modifier.testTag("INPUT_DROPDOWN_MENU_ITEM_$index") + .fillMaxWidth() + .padding(start = dropdownStartPadding(inputStyle) + 8.dp), + item = this, + selected = selectedItem == this, + contentPadding = PaddingValues( + horizontal = Spacing8, + vertical = Spacing16, + ), + onItemClick = { + currentItem = this + onItemSelected(index, this) + showDropdown = false + }, + ) + } } } }