From 8a3307c0420b6d70cd58df73435fa1b8669df387 Mon Sep 17 00:00:00 2001 From: David Perez Date: Wed, 2 Sep 2026 15:36:58 +0000 Subject: [PATCH] PM-42839, PM-42840, PM-42841, PM-42842: bug: Minor DatePicker bug fixes (#7343) --- .../addedit/VaultAddEditLicenseItems.kt | 2 + .../addedit/VaultAddEditPassportItems.kt | 2 + .../feature/generator/GeneratorScreenTest.kt | 2 +- .../feature/addedit/VaultAddEditScreenTest.kt | 133 ++++++++++++------ .../button/BitwardenTextSelectionButton.kt | 4 +- .../dialog/BitwardenDatePickerDialog.kt | 92 +++++++----- .../dropdown/BitwardenDatePickerButton.kt | 53 +++++-- ui/src/main/res/values/strings.xml | 1 + 8 files changed, 200 insertions(+), 89 deletions(-) diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt index 164837ab1b..d34a5fbf2b 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditLicenseItems.kt @@ -96,6 +96,7 @@ fun LazyListScope.vaultAddEditLicenseItems( currentDate = licenseState.dateOfBirth, onDateSelect = licenseHandlers.onDateOfBirthChange, textFieldTestTag = "LicenseDateOfBirthEntry", + allowFutureDates = false, cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() @@ -148,6 +149,7 @@ fun LazyListScope.vaultAddEditLicenseItems( currentDate = licenseState.issueDate, onDateSelect = licenseHandlers.onIssueDateChange, textFieldTestTag = "LicenseIssueDateEntry", + allowFutureDates = false, cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt index 638529e3ee..048d463cc3 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditPassportItems.kt @@ -69,6 +69,7 @@ fun LazyListScope.vaultAddEditPassportItems( currentDate = passportState.dateOfBirth, onDateSelect = passportHandlers.onDateOfBirthChange, textFieldTestTag = "PassportDateOfBirthEntry", + allowFutureDates = false, cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() @@ -188,6 +189,7 @@ fun LazyListScope.vaultAddEditPassportItems( currentDate = passportState.issueDate, onDateSelect = passportHandlers.onIssueDateChange, textFieldTestTag = "PassportIssueDateEntry", + allowFutureDates = false, cardStyle = CardStyle.Middle(), modifier = Modifier .fillMaxWidth() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt index 5ab5259fdd..0e457791c1 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/tools/feature/generator/GeneratorScreenTest.kt @@ -1010,7 +1010,7 @@ class GeneratorScreenTest : BitwardenComposeTest() { // Opens the menu composeTestRule - .onNodeWithContentDescription(label = "null. Service") + .onNodeWithContentDescription(label = "Service") .performScrollTo() .performClick() diff --git a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt index 681440a3fa..0c8cb226fb 100644 --- a/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt +++ b/app/src/test/kotlin/com/x8bit/bitwarden/ui/vault/feature/addedit/VaultAddEditScreenTest.kt @@ -2922,7 +2922,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { fun `in ItemType_License the date of birth should display the selected date from the state`() { mutableStateFlow.value = DEFAULT_STATE_LICENSE composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Date of birth") + .onNodeWithContentDescriptionAfterScroll(label = "Date of birth") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -2939,13 +2939,21 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_License clicking the date of birth clear button should trigger DateOfBirthChange with null`() { - mutableStateFlow.value = DEFAULT_STATE_LICENSE + mutableStateFlow.value = DEFAULT_STATE_LICENSE.copy( + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.License( + dateOfBirth = LocalDate.of(2026, 9, 10), + ), + isIndividualVaultDisabled = false, + ), + ) composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Date of birth") - .performClick() - - composeTestRule - .onNodeWithText(text = "Clear") + .onNodeWithContentDescriptionAfterScroll(label = "September 10, 2026. Date of birth") + .onChildren() + .filterToOne(matcher = hasText(text = "Date of birth")) + .onChildren() + .filterToOne(matcher = hasContentDescription(value = "Clear")) .performClick() verify { @@ -2959,7 +2967,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { fun `in ItemType_License the issue date should display the selected date from the state`() { mutableStateFlow.value = DEFAULT_STATE_LICENSE composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Issue date") + .onNodeWithContentDescriptionAfterScroll(label = "Issue date") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -2976,13 +2984,21 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_License clicking the issue date clear button should trigger IssueDateChange with null`() { - mutableStateFlow.value = DEFAULT_STATE_LICENSE + mutableStateFlow.value = DEFAULT_STATE_LICENSE.copy( + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.License( + issueDate = LocalDate.of(2026, 9, 10), + ), + isIndividualVaultDisabled = false, + ), + ) composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Issue date") - .performClick() - - composeTestRule - .onNodeWithText(text = "Clear") + .onNodeWithContentDescriptionAfterScroll(label = "September 10, 2026. Issue date") + .onChildren() + .filterToOne(matcher = hasText(text = "Issue date")) + .onChildren() + .filterToOne(matcher = hasContentDescription(value = "Clear")) .performClick() verify { @@ -2997,7 +3013,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { fun `in ItemType_License the expiration date should display the selected date from the state`() { mutableStateFlow.value = DEFAULT_STATE_LICENSE composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Expiration date") + .onNodeWithContentDescriptionAfterScroll(label = "Expiration date") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -3014,13 +3030,21 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_License clicking the expiration date clear button should trigger ExpirationDateChange with null`() { - mutableStateFlow.value = DEFAULT_STATE_LICENSE + mutableStateFlow.value = DEFAULT_STATE_LICENSE.copy( + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.License( + expirationDate = LocalDate.of(2026, 9, 10), + ), + isIndividualVaultDisabled = false, + ), + ) composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Expiration date") - .performClick() - - composeTestRule - .onNodeWithText(text = "Clear") + .onNodeWithContentDescriptionAfterScroll(label = "September 10, 2026. Expiration date") + .onChildren() + .filterToOne(matcher = hasText(text = "Expiration date")) + .onChildren() + .filterToOne(matcher = hasContentDescription(value = "Clear")) .performClick() verify { @@ -3201,7 +3225,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { fun `in ItemType_Passport the date of birth should display the selected date from the state`() { mutableStateFlow.value = DEFAULT_STATE_PASSPORT composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Date of birth") + .onNodeWithContentDescriptionAfterScroll(label = "Date of birth") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -3218,13 +3242,21 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_Passport clicking the date of birth clear button should trigger DateOfBirthChange with null`() { - mutableStateFlow.value = DEFAULT_STATE_PASSPORT + mutableStateFlow.value = DEFAULT_STATE_PASSPORT.copy( + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.Passport( + dateOfBirth = LocalDate.of(2026, 9, 10), + ), + isIndividualVaultDisabled = false, + ), + ) composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Date of birth") - .performClick() - - composeTestRule - .onNodeWithText(text = "Clear") + .onNodeWithContentDescriptionAfterScroll(label = "September 10, 2026. Date of birth") + .onChildren() + .filterToOne(matcher = hasText(text = "Date of birth")) + .onChildren() + .filterToOne(matcher = hasContentDescription(value = "Clear")) .performClick() verify { @@ -3234,12 +3266,11 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { } } - @Suppress("MaxLineLength") @Test fun `in ItemType_Passport the issue date should display the selected date from the state`() { mutableStateFlow.value = DEFAULT_STATE_PASSPORT composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Issue date") + .onNodeWithContentDescriptionAfterScroll(label = "Issue date") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -3256,13 +3287,21 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_Passport clicking the issue date clear button should trigger IssueDateChange with null`() { - mutableStateFlow.value = DEFAULT_STATE_PASSPORT + mutableStateFlow.value = DEFAULT_STATE_PASSPORT.copy( + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.Passport( + issueDate = LocalDate.of(2026, 9, 10), + ), + isIndividualVaultDisabled = false, + ), + ) composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Issue date") - .performClick() - - composeTestRule - .onNodeWithText(text = "Clear") + .onNodeWithContentDescriptionAfterScroll(label = "September 10, 2026. Issue date") + .onChildren() + .filterToOne(matcher = hasText(text = "Issue date")) + .onChildren() + .filterToOne(matcher = hasContentDescription(value = "Clear")) .performClick() verify { @@ -3277,7 +3316,7 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { fun `in ItemType_Passport the expiration date should display the selected date from the state`() { mutableStateFlow.value = DEFAULT_STATE_PASSPORT composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Expiration date") + .onNodeWithContentDescriptionAfterScroll(label = "Expiration date") .assertIsDisplayed() mutableStateFlow.update { currentState -> @@ -3294,13 +3333,21 @@ class VaultAddEditScreenTest : BitwardenComposeTest() { @Suppress("MaxLineLength") @Test fun `in ItemType_Passport clicking the expiration date clear button should trigger ExpirationDateChange with null`() { - mutableStateFlow.value = DEFAULT_STATE_PASSPORT + mutableStateFlow.value = DEFAULT_STATE_PASSPORT.copy( + viewState = VaultAddEditState.ViewState.Content( + common = VaultAddEditState.ViewState.Content.Common(), + type = VaultAddEditState.ViewState.Content.ItemType.Passport( + expirationDate = LocalDate.of(2026, 9, 10), + ), + isIndividualVaultDisabled = false, + ), + ) composeTestRule - .onNodeWithContentDescriptionAfterScroll(label = "null. Expiration date") - .performClick() - - composeTestRule - .onNodeWithText(text = "Clear") + .onNodeWithContentDescriptionAfterScroll(label = "September 10, 2026. Expiration date") + .onChildren() + .filterToOne(matcher = hasText(text = "Expiration date")) + .onChildren() + .filterToOne(matcher = hasContentDescription(value = "Clear")) .performClick() verify { diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt index d6d149825a..88d0f01f55 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/button/BitwardenTextSelectionButton.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.customActions +import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow @@ -113,7 +114,7 @@ fun BitwardenTextSelectionButton( .defaultMinSize(minHeight = 60.dp) .semantics { role = semanticRole - contentDescription = "$selectedOption. $label" + contentDescription = "${selectedOption?.let { "$it. " }.orEmpty()}$label" customActions = persistentListOfNotNull( helpData?.let { CustomAccessibilityAction( @@ -180,6 +181,7 @@ fun BitwardenTextSelectionButton( bitwardenTextFieldColors() }, modifier = Modifier + .semantics { this.hideFromAccessibility() } .nullableTestTag(tag = textFieldTestTag) .fillMaxWidth(), ) diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenDatePickerDialog.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenDatePickerDialog.kt index 33209ffa20..fafa73536e 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenDatePickerDialog.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenDatePickerDialog.kt @@ -4,8 +4,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidth @@ -16,6 +15,7 @@ import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.DatePicker import androidx.compose.material3.DatePickerColors import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SelectableDates import androidx.compose.material3.Surface import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable @@ -29,8 +29,10 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.bitwarden.ui.platform.components.button.BitwardenTextButton import com.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors +import com.bitwarden.ui.platform.composition.LocalClock import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme +import java.time.Clock import java.time.Instant import java.time.LocalDate import java.time.ZoneOffset @@ -41,6 +43,10 @@ import java.time.ZoneOffset * @param initialDate The initial [LocalDate] to display. * @param onDateSelect The callback invoked with the selected [LocalDate] when the user confirms. * @param onDismissRequest The callback invoked when the dialog is dismissed. + * @param allowPastDates Indicates that this date picker allows past dates. + * @param allowFutureDates Indicates that this date picker allows future dates. + * @param allowToday Indicates that this date picker allows this date to be selected. + * @param clock The system clock. */ @Suppress("LongMethod") @OptIn(ExperimentalMaterial3Api::class) @@ -49,12 +55,40 @@ fun BitwardenDatePickerDialog( initialDate: LocalDate?, onDateSelect: (LocalDate?) -> Unit, onDismissRequest: () -> Unit, + allowPastDates: Boolean = true, + allowFutureDates: Boolean = true, + allowToday: Boolean = true, + clock: Clock = LocalClock.current, ) { val datePickerState = rememberDatePickerState( initialSelectedDateMillis = initialDate ?.atStartOfDay(ZoneOffset.UTC) ?.toInstant() ?.toEpochMilli(), + selectableDates = object : SelectableDates { + override fun isSelectableDate( + utcTimeMillis: Long, + ): Boolean { + val today = LocalDate.now(clock) + val date = Instant.ofEpochMilli(utcTimeMillis).atZone(ZoneOffset.UTC).toLocalDate() + return when { + date.isBefore(today) -> allowPastDates + date.isAfter(today) -> allowFutureDates + else -> allowToday + } + } + + override fun isSelectableYear( + year: Int, + ): Boolean { + val clockYear = clock.instant().atZone(clock.zone).year + return when { + clockYear > year -> allowPastDates + clockYear < year -> allowFutureDates + else -> true + } + } + }, ) BasicAlertDialog( onDismissRequest = onDismissRequest, @@ -86,38 +120,32 @@ fun BitwardenDatePickerDialog( colors = colors, ) } - Row(modifier = Modifier.padding(bottom = 8.dp, start = 12.dp)) { + FlowRow( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp, start = 16.dp, end = 16.dp), + ) { BitwardenTextButton( - label = stringResource(id = BitwardenString.clear), - onClick = { onDateSelect(null) }, - modifier = Modifier.testTag(tag = "ClearAlertButton"), + label = stringResource(id = BitwardenString.cancel), + onClick = onDismissRequest, + modifier = Modifier.testTag(tag = "DismissAlertButton"), + ) + BitwardenTextButton( + label = stringResource(id = BitwardenString.okay), + onClick = { + datePickerState + .selectedDateMillis + ?.let { millis -> + Instant + .ofEpochMilli(millis) + .atZone(ZoneOffset.UTC) + .toLocalDate() + } + .let(onDateSelect) + }, + modifier = Modifier.testTag(tag = "AcceptAlertButton"), ) - Spacer(modifier = Modifier.weight(weight = 1f)) - FlowRow( - horizontalArrangement = Arrangement.End, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - BitwardenTextButton( - label = stringResource(id = BitwardenString.cancel), - onClick = onDismissRequest, - modifier = Modifier.testTag(tag = "DismissAlertButton"), - ) - BitwardenTextButton( - label = stringResource(id = BitwardenString.okay), - onClick = { - datePickerState - .selectedDateMillis - ?.let { millis -> - Instant - .ofEpochMilli(millis) - .atZone(ZoneOffset.UTC) - .toLocalDate() - } - .let(onDateSelect) - }, - modifier = Modifier.testTag(tag = "AcceptAlertButton"), - ) - } } } } diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenDatePickerButton.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenDatePickerButton.kt index 1bbb5a8ffd..a5eadabe65 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenDatePickerButton.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenDatePickerButton.kt @@ -2,22 +2,30 @@ package com.bitwarden.ui.platform.components.dropdown import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Icon import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import com.bitwarden.core.data.util.toFormattedDateStyle +import com.bitwarden.ui.platform.components.animation.AnimateNullableContentVisibility +import com.bitwarden.ui.platform.components.button.BitwardenStandardIconButton import com.bitwarden.ui.platform.components.button.BitwardenTextSelectionButton import com.bitwarden.ui.platform.components.button.model.BitwardenHelpButtonData import com.bitwarden.ui.platform.components.dialog.BitwardenDatePickerDialog import com.bitwarden.ui.platform.components.model.CardStyle +import com.bitwarden.ui.platform.components.util.rememberVectorPainter import com.bitwarden.ui.platform.composition.LocalClock +import com.bitwarden.ui.platform.resource.BitwardenDrawable +import com.bitwarden.ui.platform.resource.BitwardenString import com.bitwarden.ui.platform.theme.BitwardenTheme import java.time.Clock import java.time.LocalDate @@ -36,10 +44,10 @@ import java.time.format.FormatStyle * @param helpData An optional [BitwardenHelpButtonData], representing the help button. * @param insets Inner padding to be applied within the card. * @param textFieldTestTag The optional test tag associated with the inner text field. - * @param actionsPadding Padding to be applied to the [actions] block. - * @param actions A lambda containing the set of actions (usually icons or similar) to display - * in the trailing side. This lambda extends [RowScope], allowing flexibility in defining the - * layout of the actions. + * @param allowPastDates Indicates that the date picker allows past dates. + * @param allowFutureDates Indicates that the date picker allows future dates. + * @param allowToday Indicates that this date picker allows this date to be selected. + * @param clock The system clock. */ @Composable fun BitwardenDatePickerButton( @@ -53,11 +61,13 @@ fun BitwardenDatePickerButton( helpData: BitwardenHelpButtonData? = null, insets: PaddingValues = PaddingValues(), textFieldTestTag: String? = null, - actionsPadding: PaddingValues = PaddingValues(end = 4.dp), + allowPastDates: Boolean = true, + allowFutureDates: Boolean = true, + allowToday: Boolean = true, clock: Clock = LocalClock.current, - actions: @Composable RowScope.() -> Unit = {}, ) { var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) } + val openCalendarString = stringResource(id = BitwardenString.open_calendar) BitwardenTextSelectionButton( label = label, selectedOption = currentDate?.toFormattedDateStyle( @@ -67,14 +77,29 @@ fun BitwardenDatePickerButton( onClick = { shouldShowDialog = true }, cardStyle = cardStyle, enabled = isEnabled, - showChevron = true, + showChevron = false, supportingContent = supportingContent, helpData = helpData, insets = insets, textFieldTestTag = textFieldTestTag, - actionsPadding = actionsPadding, - actions = actions, - modifier = modifier, + actions = { + AnimateNullableContentVisibility(targetState = currentDate) { + BitwardenStandardIconButton( + vectorIconRes = BitwardenDrawable.ic_clear, + contentDescription = stringResource(id = BitwardenString.clear), + onClick = { onDateSelect(null) }, + isEnabled = isEnabled, + ) + } + Icon( + painter = rememberVectorPainter(id = BitwardenDrawable.ic_chevron_down), + contentDescription = null, + modifier = Modifier.minimumInteractiveComponentSize(), + ) + }, + modifier = modifier.semantics(mergeDescendants = true) { + this.onClick(label = openCalendarString, action = null) + }, ) if (shouldShowDialog) { BitwardenDatePickerDialog( @@ -84,6 +109,10 @@ fun BitwardenDatePickerButton( shouldShowDialog = false }, onDismissRequest = { shouldShowDialog = false }, + allowPastDates = allowPastDates, + allowFutureDates = allowFutureDates, + allowToday = allowToday, + clock = clock, ) } } diff --git a/ui/src/main/res/values/strings.xml b/ui/src/main/res/values/strings.xml index eaf9d5e958..5af3d2dde2 100644 --- a/ui/src/main/res/values/strings.xml +++ b/ui/src/main/res/values/strings.xml @@ -363,6 +363,7 @@ Scanning will happen automatically. Passphrase Word separator Clear + open calendar Generator There are no folders to list. Fingerprint phrase