Repository: dhis2/dhis2-mobile-ui
Pull request #419: fix: [ANDROAPP-6885] Scroll upwards when hiding keyboard
Author: login: xavimolloy
Created: 2025-05-23T11:57:40Z | Merged: 2025-05-27T07:18:00Z



Diff:
diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/DataTable.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/DataTable.kt
index 612aebca1..3a89bba86 100644
--- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/DataTable.kt
+++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/DataTable.kt
@@ -206,11 +206,11 @@ fun DataTable(
     ) {
         Table(
             tableList = tableList,
-            tableHeaderRow = { index, tableModel, isTableScrolled ->
+            tableHeaderRow = { index, tableModel ->
                 val isSingleValue = tableModel.tableRows.firstOrNull()?.values?.size == 1
                 TableHeaderRow(
                     modifier = Modifier
-                        .zIndex(if (isTableScrolled) 2f else 0f)
+                        .zIndex(2f)
                         .background(Color.White)
                         .padding(start = Spacing.Spacing16, end = Spacing.Spacing16),
                     cornerUiState = TableCornerUiState(
diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/TableDimensions.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/TableDimensions.kt
index 4b5de7dd6..7187468b8 100644
--- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/TableDimensions.kt
+++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/TableDimensions.kt
@@ -39,7 +39,7 @@ const val GROUPED_ID = "GROUPED"
 data class TableDimensions(
     val tableHorizontalPadding: Dp = Spacing.Spacing16,
     val tableVerticalPadding: Dp = Spacing.Spacing16,
-    val defaultCellWidth: Int = 160,
+    val defaultCellWidth: Int = 260,
     val defaultCellHeight: Dp = Spacing.Spacing40,
     val defaultRowHeaderWidth: Int = 275,
     val defaultHeaderHeight: Int = 36,
diff --git a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/internal/Table.kt b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/internal/Table.kt
index b7d9b4281..9fb45d3e9 100644
--- a/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/internal/Table.kt
+++ b/designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/internal/Table.kt
@@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.core.animateIntOffsetAsState
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.animateScrollBy
 import androidx.compose.foundation.layout.Arrangement.spacedBy
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
@@ -16,7 +17,6 @@ import androidx.compose.foundation.layout.offset
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyListScope
-import androidx.compose.foundation.lazy.LazyListState
 import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
@@ -60,7 +60,9 @@ import org.hisp.dhis.mobile.ui.designsystem.component.table.ui.internal.extensio
 import org.hisp.dhis.mobile.ui.designsystem.resource.provideStringResource
 import org.hisp.dhis.mobile.ui.designsystem.theme.Spacing
 import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor
-import org.jetbrains.compose.resources.ExperimentalResourceApi
+
+const val SELECTED_CELL_INDEX_SCROLL_OFFSET = -2
+const val SCROLL_OFFSET_STICKY_HEADER = -250f
 
 /**
  * Composable function to display a table.
@@ -74,7 +76,7 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
  * @param maxRowColumnHeaders The maximum number of row column headers.
  * @param contentPadding The padding values for the content of the table.
  */
-@OptIn(ExperimentalFoundationApi::class, ExperimentalResourceApi::class)
+@OptIn(ExperimentalFoundationApi::class)
 @Composable
 internal fun Table(
     tableList: List<TableModel>,
@@ -82,7 +84,6 @@ internal fun Table(
         (
             index: Int,
             tableModel: TableModel,
-            isTableScrolled: Boolean,
         ) -> Unit
     )? = null,
     tableItemRow: @Composable (
@@ -124,7 +125,7 @@ internal fun Table(
                 tableList.forEachIndexed { tableIndex, tableModel ->
                     val isLastTable = tableList.lastIndex == tableIndex
                     tableHeaderRow?.takeIf { tableModel.hasHeaders() }
-                        ?.invoke(tableIndex, tableModel, false)
+                        ?.invoke(tableIndex, tableModel)
                     tableModel.tableRows.forEachIndexed { rowIndex, tableRowModel ->
                         val isLastRow = tableModel.tableRows.lastIndex == rowIndex
                         tableItemRow?.invoke(tableIndex, tableModel, listOf(tableRowModel))
@@ -162,10 +163,18 @@ internal fun Table(
                     itemInfo.index == selectedIndex && itemInfo.offset >= 0
                 }
                 val shouldScroll = isItemVisible && (isCellSelection || isKeyboardOpen)
-                verticalScrollState.animateToIf(
-                    selectedIndex,
-                    shouldScroll,
-                )
+                if (shouldScroll) {
+                    verticalScrollState.animateScrollToItem(
+                        selectedIndex,
+                        SELECTED_CELL_INDEX_SCROLL_OFFSET,
+                    )
+                }
+            }
+
+            LaunchedEffect(keyboardState) {
+                if (tableSelection is TableSelection.CellSelection && keyboardState == Keyboard.Closed) {
+                    verticalScrollState.animateScrollBy(SCROLL_OFFSET_STICKY_HEADER)
+                }
             }
 
             val isScrolled by remember {
@@ -225,7 +234,7 @@ internal fun Table(
                     tableList.forEachIndexed { tableIndex, tableModel ->
                         val isLastTable = tableList.lastIndex == tableIndex
                         fixedStickyHeader(
-                            fixHeader = keyboardState == Keyboard.Closed,
+                            fixHeader = true,
                             key = tableModel.id,
                         ) {
                             val isFirstVisibleStickyHeader by remember {
@@ -238,7 +247,6 @@ internal fun Table(
                             tableHeaderRow?.takeIf { tableModel.hasHeaders() }?.invoke(
                                 tableIndex,
                                 tableModel,
-                                isFirstVisibleStickyHeader && isScrolled,
                             )
                         }
                         val rowItems =
@@ -255,7 +263,7 @@ internal fun Table(
                                 .last()
 
                         fixedStickyHeader(
-                            fixHeader = keyboardState == Keyboard.Closed,
+                            fixHeader = true,
                             key = "${tableModel.id}_sticky_last_row",
                         ) {
                             tableItemRow?.invoke(
@@ -279,7 +287,7 @@ internal fun Table(
                         if (!tableConfiguration.groupTables) {
                             stickyFooter(
                                 key = "${tableModel.id}_footer",
-                                showFooter = keyboardState == Keyboard.Closed,
+                                showFooter = true,
                             )
                         }
                     }
@@ -321,16 +329,6 @@ internal fun Table(
     }
 }
 
-private suspend fun LazyListState.animateToIf(index: Int, condition: Boolean) {
-    if (condition) {
-        apply {
-            if (index >= 0) {
-                animateScrollToItem(index, 200)
-            }
-        }
-    }
-}
-
 @OptIn(ExperimentalFoundationApi::class)
 private fun LazyListScope.stickyFooter(
     key: String?,
diff --git a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TableTest.kt b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TableTest.kt
index e0081df87..16138a348 100644
--- a/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TableTest.kt
+++ b/designsystem/src/desktopTest/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/TableTest.kt
@@ -54,8 +54,8 @@ class TableTest {
             initTable(table)
             val firstTableId = table[0].id
 
-            clickOnHeaderElement(firstTableId, 2, 3)
-            assertColumnHeaderBackgroundColor(firstTableId, 2, 3, SurfaceColor.Primary)
+            clickOnHeaderElement(firstTableId, 2, 2)
+            assertColumnHeaderBackgroundColor(firstTableId, 2, 2, SurfaceColor.Primary)
         }
     }
 
diff --git a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TableCellSnapshotTest_launchTableCell.png b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TableCellSnapshotTest_launchTableCell.png
index 2db4face0..5d9c84fc1 100644
--- a/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TableCellSnapshotTest_launchTableCell.png
+++ b/designsystem/src/test/snapshots/images/org.hisp.dhis.mobile.ui.designsystem_TableCellSnapshotTest_launchTableCell.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:af862286f046c3cd5deedf6a9b74964decf0b44168f695b96e78eb4c62632d34
-size 12067
+oid sha256:9a7b576940e00fb7fe5f9a8413d3d303a1e10b4b1f6f0e06fa78d1146d5bf30b
+size 11533


Reviews:
user: login: Balcan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T13:34:20Z
user: login: xavimolloy
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-26T05:44:38Z
user: login: andresmr
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T08:46:46Z
user: login: Balcan
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T08:58:33Z
user: login: Balcan
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T10:38:18Z
user: login: mmmateos
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-26T11:29:42Z
user: login: andresmr
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-26T11:35:23Z

Review comments:
diff_hunk: @@ -162,10 +163,18 @@ internal fun Table(
                     itemInfo.index == selectedIndex && itemInfo.offset >= 0
                 }
                 val shouldScroll = isItemVisible && (isCellSelection || isKeyboardOpen)
-                verticalScrollState.animateToIf(
-                    selectedIndex,
-                    shouldScroll,
-                )
+                if (shouldScroll) {
+                    verticalScrollState.animateScrollToItem(
+                        selectedIndex,
+                        -2,
path: designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/internal/Table.kt
user: login: Balcan
body: Can we extract these "magic numbers" to constants ?
created_at: 2025-05-23T13:34:12Z
author_association: COLLABORATOR
diff_hunk: @@ -162,10 +163,18 @@ internal fun Table(
                     itemInfo.index == selectedIndex && itemInfo.offset >= 0
                 }
                 val shouldScroll = isItemVisible && (isCellSelection || isKeyboardOpen)
-                verticalScrollState.animateToIf(
-                    selectedIndex,
-                    shouldScroll,
-                )
+                if (shouldScroll) {
+                    verticalScrollState.animateScrollToItem(
+                        selectedIndex,
+                        -2,
path: designsystem/src/commonMain/kotlin/org/hisp/dhis/mobile/ui/designsystem/component/table/ui/internal/Table.kt
user: login: xavimolloy
body: Good idea!
created_at: 2025-05-26T05:44:37Z
author_association: COLLABORATOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-26T12:21:33Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=dhis2_dhis2-mobile-ui&pullRequest=419) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=dhis2_dhis2-mobile-ui&pullRequest=419&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=dhis2_dhis2-mobile-ui&pullRequest=419&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=dhis2_dhis2-mobile-ui&pullRequest=419&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=dhis2_dhis2-mobile-ui&pullRequest=419&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=dhis2_dhis2-mobile-ui&pullRequest=419&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=dhis2_dhis2-mobile-ui&pullRequest=419)


author_association: NONE