From 3f774e10045098c3848673a0d717dd6e375a2ed6 Mon Sep 17 00:00:00 2001 From: dadachi Date: Sat, 17 Oct 2026 08:10:25 +0900 Subject: [PATCH] Let the last row of FAB lists scroll above the button The last row of Manage Item Tags (and the Delete button its swipe reveals) stayed under the + button, because the list had no bottom content padding. The lists with a floating action button (Shops, Manage Item Tags) now end with bottom content padding that clears it (FabClearance), as in MyTurnTag Creator. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y3Tt4YrVgQRxh5MCpiiric --- .../ui/common/FabClearance.kt | 20 +++++++++++ .../item_tag_list/ItemTagListView.kt | 5 ++- .../ui/shops/ShopListView.kt | 5 ++- .../item_tag_list/ItemTagListViewModelTest.kt | 33 +++++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/common/FabClearance.kt diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/common/FabClearance.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/common/FabClearance.kt new file mode 100644 index 0000000..3ec85c3 --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/common/FabClearance.kt @@ -0,0 +1,20 @@ +package com.nativeapptemplate.nativeapptemplatefree.ui.common + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +/** + * Space below the last row of a list on a screen with a floating action button, so the last row + * (and the actions it reveals when swiped) can scroll above the button instead of staying under it. + * The tallest button is SubmitFab (64.dp; FloatingActionButton is 56.dp), the Scaffold places it + * 16.dp above the bottom, and 16.dp more keeps the row clear of it: 64 + 16 + 16 = 96. + */ +val FabClearance = 96.dp + +/** The lists' 24.dp margin, without the bottom: the bottom is [fabClearingContentPadding]. */ +val fabListModifier: Modifier = Modifier.padding(start = 24.dp, top = 24.dp, end = 24.dp) + +/** Content padding for a list on a screen with a floating action button; see [FabClearance]. */ +val fabClearingContentPadding = PaddingValues(bottom = FabClearance) diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListView.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListView.kt index f864368..f713fe7 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListView.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListView.kt @@ -55,6 +55,8 @@ import com.nativeapptemplate.nativeapptemplatefree.ui.common.LoadingView import com.nativeapptemplate.nativeapptemplatefree.ui.common.MainButtonView import com.nativeapptemplate.nativeapptemplatefree.ui.common.SnackbarMessageEffect import com.nativeapptemplate.nativeapptemplatefree.ui.common.SwipeableItemWithActions +import com.nativeapptemplate.nativeapptemplatefree.ui.common.fabClearingContentPadding +import com.nativeapptemplate.nativeapptemplatefree.ui.common.fabListModifier @Composable internal fun ItemTagListView( @@ -195,7 +197,8 @@ private fun ItemTagListContentView( ) { LazyColumn( state = listState, - modifier = Modifier.padding(24.dp), + modifier = fabListModifier, + contentPadding = fabClearingContentPadding, ) { item { Text( diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListView.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListView.kt index 4b3906c..437b2cf 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListView.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shops/ShopListView.kt @@ -48,6 +48,8 @@ import com.nativeapptemplate.nativeapptemplatefree.ui.common.ErrorView import com.nativeapptemplate.nativeapptemplatefree.ui.common.LoadingView import com.nativeapptemplate.nativeapptemplatefree.ui.common.MainButtonView import com.nativeapptemplate.nativeapptemplatefree.ui.common.SnackbarMessageEffect +import com.nativeapptemplate.nativeapptemplatefree.ui.common.fabClearingContentPadding +import com.nativeapptemplate.nativeapptemplatefree.ui.common.fabListModifier @Composable internal fun ShopListView( @@ -164,7 +166,8 @@ private fun ShopListContentView( .padding(padding), ) { LazyColumn( - Modifier.padding(24.dp), + modifier = fabListModifier, + contentPadding = fabClearingContentPadding, ) { item { if (!uiState.didShowTapShopBelowTip) { diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModelTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModelTest.kt index 0782830..1dfe26a 100644 --- a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModelTest.kt +++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/ui/shop_settings/item_tag_list/ItemTagListViewModelTest.kt @@ -2,14 +2,17 @@ package com.nativeapptemplate.nativeapptemplatefree.ui.shop_settings.item_tag_li import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.test.getUnclippedBoundsInRoot import androidx.compose.ui.test.hasScrollToIndexAction import androidx.compose.ui.test.hasScrollToKeyAction import androidx.compose.ui.test.junit4.v2.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.test.performScrollToKey import androidx.lifecycle.SavedStateHandle import androidx.navigation.testing.invoke +import com.nativeapptemplate.nativeapptemplatefree.R import com.nativeapptemplate.nativeapptemplatefree.data.item_tag.ItemTagRepository import com.nativeapptemplate.nativeapptemplatefree.model.Attributes import com.nativeapptemplate.nativeapptemplatefree.model.Data @@ -36,6 +39,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment /** * These tests use Robolectric because the subject under test (the ViewModel) uses @@ -298,6 +302,35 @@ class ItemTagListViewModelTest { composeTestRule.onNodeWithText("A007").assertExists() } + // The last row (and the Delete button its swipe reveals) must be able to scroll above the "+" + // button; without bottom content padding the list ends behind the button and the row stays under it. + @Test + fun lastRow_scrollsAboveTheAddButton() { + // 20 tags on one page: longer than the screen, so the list scrolls. + itemTagRepository.itemTagsForPage = { page -> pagedItemTags(page = page ?: 1, totalPages = 1, perPage = 20) } + shopRepository.sendShop(testInputShop) + composeTestRule.setContent { + val uiState by viewModel.uiState.collectAsState() + ItemTagListView( + viewModel = viewModel, + uiState = uiState, + onItemClick = {}, + onAddItemTagClick = {}, + onBackClick = {}, + ) + } + composeTestRule.runOnIdle { viewModel.reload() } + + // Scrolling to the last tag scrolls the list to its end. pagedItemTags names tag 20 "A020". + composeTestRule.onNode(hasScrollToKeyAction()).performScrollToKey("item-tag-20") + val context = RuntimeEnvironment.getApplication() + val lastRowBottom = composeTestRule.onNodeWithText("A020").getUnclippedBoundsInRoot().bottom + val addButtonTop = composeTestRule.onNodeWithContentDescription(context.getString(R.string.label_add_item_tag)) + .getUnclippedBoundsInRoot().top + + assertTrue("last row ends at $lastRowBottom, the add button starts at $addButtonTop", lastRowBottom <= addButtonTop) + } + // The API rejects it: the coded error is shown, the spinner stops, and the list stays. @Test fun deleteItemTag_whenTheApiRejectsIt_showsTheErrorAndKeepsTheList() = runTest {