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