[PM-42843] fix: Stop date picker flashing when switching to calendar (#7536)

This commit is contained in:
André Bispo
2026-10-27 16:17:31 +00:00
committed by GitHub
parent 4cca57b0ce
commit 29f9b83902
3 changed files with 101 additions and 0 deletions
@@ -0,0 +1,22 @@
package com.bitwarden.ui.platform.base.util
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.layout.LocalPinnableContainer
import androidx.compose.ui.layout.PinnableContainer
/**
* Pins the calling composable in its [pinnableContainer] for as long as it stays in the
* composition, so a lazy layout keeps it composed even after it scrolls out of view.
*
* @param pinnableContainer The [PinnableContainer] to pin, typically the current lazy item.
*/
@Composable
fun PinThisComposable(
pinnableContainer: PinnableContainer? = LocalPinnableContainer.current,
) {
DisposableEffect(pinnableContainer) {
val pinnedHandle = pinnableContainer?.pin()
onDispose { pinnedHandle?.release() }
}
}
@@ -16,6 +16,7 @@ import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.core.data.util.toFormattedDateStyle
import com.bitwarden.ui.platform.base.util.PinThisComposable
import com.bitwarden.ui.platform.components.animation.AnimateNullableContentVisibility
import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton
import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton
@@ -102,6 +103,10 @@ fun BitwardenDatePickerButton(
},
)
if (shouldShowDialog) {
// Keep this button composed while the dialog is showing. In a lazy list, the button can
// scroll out of view when the soft keyboard opens, and disposing it would dismiss the
// dialog and recreate it once the keyboard closes.
PinThisComposable()
BitwardenDatePickerDialog(
initialDate = currentDate,
onDateSelect = { date ->
@@ -0,0 +1,74 @@
package com.bitwarden.ui.platform.components.dropdown
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.platform.app.InstrumentationRegistry
import com.bitwarden.ui.platform.base.BaseComposeTest
import com.bitwarden.ui.platform.theme.BitwardenTheme
import kotlinx.coroutines.runBlocking
import org.junit.Test
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
class BitwardenDatePickerButtonTest : BaseComposeTest() {
@Test
fun `dialog should stay open when switching modes after the button scrolls away`() {
// Match a device, where tapping does not move focus to the tapped button.
InstrumentationRegistry.getInstrumentation().setInTouchMode(true)
lateinit var lazyListState: LazyListState
setTestContent {
BitwardenTheme {
lazyListState = rememberLazyListState()
LazyColumn(state = lazyListState) {
item {
BitwardenDatePickerButton(
label = LABEL,
currentDate = null,
onDateSelect = {},
cardStyle = null,
clock = FIXED_CLOCK,
)
}
items(count = 50) {
Spacer(modifier = Modifier.height(height = 200.dp))
}
}
}
}
composeTestRule.onNodeWithText(text = LABEL).performClick()
composeTestRule
.onNodeWithContentDescription(label = "Switch to text input mode")
.performClick()
composeTestRule.onNodeWithText(text = "Entered date").assertIsDisplayed()
composeTestRule.runOnUiThread {
runBlocking { lazyListState.scrollToItem(index = 40) }
}
composeTestRule
.onNodeWithContentDescription(label = "Switch to calendar input mode")
.performClick()
composeTestRule.onNodeWithTag(testTag = "DatePickerDialog").assertIsDisplayed()
composeTestRule.onNodeWithText(text = "Selected date").assertIsDisplayed()
}
}
private const val LABEL: String = "Date of birth"
private val FIXED_CLOCK: Clock = Clock.fixed(
Instant.parse("2026-10-26T12:00:00Z"),
ZoneOffset.UTC,
)