Compare commits

...

1 Commits

Author SHA1 Message Date
David Perez
60487c18d1 PM-37573: Create DatePickerDialog 2026-05-15 15:02:14 -05:00
7 changed files with 250 additions and 10 deletions

View File

@@ -23,6 +23,7 @@ import com.bitwarden.cxf.ui.composition.LocalCredentialExchangeRequestValidator
import com.bitwarden.cxf.validator.CredentialExchangeRequestValidator
import com.bitwarden.cxf.validator.dsl.credentialExchangeRequestValidator
import com.bitwarden.ui.platform.composition.LocalCardTextAnalyzer
import com.bitwarden.ui.platform.composition.LocalClock
import com.bitwarden.ui.platform.composition.LocalExitManager
import com.bitwarden.ui.platform.composition.LocalIntentManager
import com.bitwarden.ui.platform.composition.LocalQrCodeAnalyzer
@@ -134,11 +135,6 @@ val LocalBiometricsManager: ProvidableCompositionLocal<BiometricsManager> = comp
error("CompositionLocal BiometricsManager not present")
}
/**
* Provides access to the clock throughout the app.
*/
val LocalClock: ProvidableCompositionLocal<Clock> = compositionLocalOf { Clock.systemDefaultZone() }
/**
* Provides access to the Auth Tab launchers throughout the app.
*/

View File

@@ -13,10 +13,10 @@ import androidx.compose.ui.unit.dp
import com.bitwarden.core.data.util.toFormattedDateTimeStyle
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.composition.LocalClock
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.composition.LocalClock
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.parcelize.Parcelize

View File

@@ -11,10 +11,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
import com.bitwarden.ui.platform.components.model.CardStyle
import com.bitwarden.ui.platform.composition.LocalClock
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.util.Text
import com.bitwarden.ui.util.asText
import com.x8bit.bitwarden.ui.platform.composition.LocalClock
import kotlinx.collections.immutable.toImmutableList
import java.time.Clock
import java.time.Instant

View File

@@ -0,0 +1,132 @@
package com.bitwarden.ui.platform.components.dialog
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerColors
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.compose.ui.tooling.preview.Preview
import com.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.bitwarden.ui.platform.components.field.color.bitwardenTextFieldColors
import com.bitwarden.ui.platform.resource.BitwardenString
import com.bitwarden.ui.platform.theme.BitwardenTheme
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
/**
* A custom composable representing a dialog that displays a date picker.
*
* @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.
*/
@Composable
fun BitwardenDatePickerDialog(
initialDate: LocalDate?,
onDateSelect: (LocalDate?) -> Unit,
onDismissRequest: () -> Unit,
) {
val datePickerState = rememberDatePickerState(
initialSelectedDateMillis = initialDate
?.atStartOfDay(ZoneOffset.UTC)
?.toInstant()
?.toEpochMilli(),
)
DatePickerDialog(
onDismissRequest = onDismissRequest,
confirmButton = {
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"),
)
},
dismissButton = {
BitwardenTextButton(
label = stringResource(id = BitwardenString.clear),
onClick = { onDateSelect(null) },
contentColor = BitwardenTheme.colorScheme.status.error,
modifier = Modifier.testTag(tag = "ClearButton"),
)
BitwardenTextButton(
label = stringResource(id = BitwardenString.cancel),
onClick = onDismissRequest,
modifier = Modifier.testTag(tag = "DismissAlertButton"),
)
},
shape = BitwardenTheme.shapes.dialog,
colors = bitwardenDatePickerColors(),
modifier = Modifier.semantics {
testTagsAsResourceId = true
testTag = "DatePickerDialog"
},
) {
DatePicker(
state = datePickerState,
colors = bitwardenDatePickerColors(),
)
}
}
@Composable
private fun bitwardenDatePickerColors(): DatePickerColors = DatePickerColors(
containerColor = BitwardenTheme.colorScheme.background.primary,
titleContentColor = BitwardenTheme.colorScheme.text.secondary,
headlineContentColor = BitwardenTheme.colorScheme.text.primary,
weekdayContentColor = BitwardenTheme.colorScheme.text.secondary,
subheadContentColor = BitwardenTheme.colorScheme.text.secondary,
navigationContentColor = BitwardenTheme.colorScheme.icon.primary,
yearContentColor = BitwardenTheme.colorScheme.text.primary,
disabledYearContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
currentYearContentColor = BitwardenTheme.colorScheme.text.primary,
selectedYearContentColor = BitwardenTheme.colorScheme.filledButton.foreground,
selectedYearContainerColor = BitwardenTheme.colorScheme.filledButton.background,
disabledSelectedYearContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledSelectedYearContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
dayContentColor = BitwardenTheme.colorScheme.text.primary,
disabledDayContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledSelectedDayContentColor = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledSelectedDayContainerColor = BitwardenTheme.colorScheme.filledButton.backgroundDisabled,
selectedDayContentColor = BitwardenTheme.colorScheme.filledButton.foreground,
selectedDayContainerColor = BitwardenTheme.colorScheme.filledButton.background,
todayContentColor = BitwardenTheme.colorScheme.text.primary,
todayDateBorderColor = BitwardenTheme.colorScheme.filledButton.background,
dividerColor = BitwardenTheme.colorScheme.stroke.divider,
dayInSelectionRangeContainerColor = BitwardenTheme.colorScheme.filledButton.background,
dayInSelectionRangeContentColor = BitwardenTheme.colorScheme.text.primary,
dateTextFieldColors = bitwardenTextFieldColors(
focusedIndicatorColor = BitwardenTheme.colorScheme.outlineButton.border,
unfocusedIndicatorColor = BitwardenTheme.colorScheme.outlineButton.border,
disabledIndicatorColor = BitwardenTheme.colorScheme.outlineButton.border,
),
)
@Suppress("MagicNumber")
@Preview
@Composable
private fun BitwardenDatePickerDialog_preview() {
BitwardenTheme {
BitwardenDatePickerDialog(
initialDate = LocalDate.of(2026, 5, 2),
onDateSelect = {},
onDismissRequest = {},
)
}
}

View File

@@ -0,0 +1,103 @@
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.OutlinedTextField
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.bitwarden.core.data.util.toFormattedDateStyle
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.composition.LocalClock
import com.bitwarden.ui.platform.theme.BitwardenTheme
import java.time.Clock
import java.time.LocalDate
import java.time.format.FormatStyle
/**
* A button that displays a selected date and opens a date picker dialog when clicked.
*
* @param label The descriptive text label for the [OutlinedTextField].
* @param currentDate The currently selected [LocalDate] value.
* @param onDateSelect A lambda invoked with the newly selected [LocalDate] when confirmed.
* @param cardStyle Indicates the type of card style to be applied.
* @param modifier A [Modifier] that you can use to apply custom modifications to the composable.
* @param isEnabled Whether the button is enabled.
* @param supportingContent An optional supporting content that will appear below the button.
* @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.
*/
@Composable
fun BitwardenDatePickerButton(
label: String,
currentDate: LocalDate?,
onDateSelect: (LocalDate?) -> Unit,
cardStyle: CardStyle?,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
supportingContent: @Composable (ColumnScope.() -> Unit)? = null,
helpData: BitwardenHelpButtonData? = null,
insets: PaddingValues = PaddingValues(),
textFieldTestTag: String? = null,
actionsPadding: PaddingValues = PaddingValues(end = 4.dp),
clock: Clock = LocalClock.current,
actions: @Composable RowScope.() -> Unit = {},
) {
var shouldShowDialog by rememberSaveable { mutableStateOf(value = false) }
BitwardenTextSelectionButton(
label = label,
selectedOption = currentDate?.toFormattedDateStyle(
dateStyle = FormatStyle.LONG,
clock = clock,
),
onClick = { shouldShowDialog = true },
cardStyle = cardStyle,
enabled = isEnabled,
showChevron = true,
supportingContent = supportingContent,
helpData = helpData,
insets = insets,
textFieldTestTag = textFieldTestTag,
actionsPadding = actionsPadding,
actions = actions,
modifier = modifier,
)
if (shouldShowDialog) {
BitwardenDatePickerDialog(
initialDate = currentDate,
onDateSelect = { date ->
onDateSelect(date)
shouldShowDialog = false
},
onDismissRequest = { shouldShowDialog = false },
)
}
}
@Suppress("MagicNumber")
@Preview
@Composable
private fun BitwardenDatePickerButton_preview() {
BitwardenTheme {
BitwardenDatePickerButton(
label = "Date of birth",
currentDate = LocalDate.of(2026, 6, 15),
onDateSelect = {},
cardStyle = CardStyle.Full,
)
}
}

View File

@@ -31,6 +31,9 @@ fun bitwardenTextFieldColors(
disabledLabelColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
disabledPlaceholderColor: Color = BitwardenTheme.colorScheme.text.secondary,
disabledSupportingTextColor: Color = BitwardenTheme.colorScheme.filledButton.foregroundDisabled,
focusedIndicatorColor: Color = Color.Transparent,
unfocusedIndicatorColor: Color = Color.Transparent,
disabledIndicatorColor: Color = Color.Transparent,
): TextFieldColors = TextFieldColors(
focusedTextColor = textColor,
unfocusedTextColor = textColor,
@@ -46,9 +49,9 @@ fun bitwardenTextFieldColors(
handleColor = BitwardenTheme.colorScheme.stroke.border,
backgroundColor = BitwardenTheme.colorScheme.stroke.border.copy(alpha = 0.4f),
),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
disabledIndicatorColor = Color.Transparent,
focusedIndicatorColor = focusedIndicatorColor,
unfocusedIndicatorColor = unfocusedIndicatorColor,
disabledIndicatorColor = disabledIndicatorColor,
errorIndicatorColor = BitwardenTheme.colorScheme.status.error,
focusedLeadingIconColor = BitwardenTheme.colorScheme.icon.primary,
unfocusedLeadingIconColor = BitwardenTheme.colorScheme.icon.primary,

View File

@@ -6,6 +6,7 @@ import com.bitwarden.ui.platform.feature.cardscanner.util.CardTextAnalyzer
import com.bitwarden.ui.platform.feature.qrcodescan.util.QrCodeAnalyzer
import com.bitwarden.ui.platform.manager.IntentManager
import com.bitwarden.ui.platform.manager.exit.ExitManager
import java.time.Clock
/**
* Provides access to the exit manager throughout the app.
@@ -29,6 +30,11 @@ val LocalCardTextAnalyzer: ProvidableCompositionLocal<CardTextAnalyzer> =
error("CompositionLocal LocalCardTextAnalyzer not present")
}
/**
* Provides access to the clock throughout the app.
*/
val LocalClock: ProvidableCompositionLocal<Clock> = compositionLocalOf { Clock.systemDefaultZone() }
/**
* Provides access to the QR Code Analyzer throughout the app.
*/