diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt index 8df5b79302..d906bea0ee 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/platform/composition/LocalManagerProvider.kt @@ -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 = comp error("CompositionLocal BiometricsManager not present") } -/** - * Provides access to the clock throughout the app. - */ -val LocalClock: ProvidableCompositionLocal = compositionLocalOf { Clock.systemDefaultZone() } - /** * Provides access to the Auth Tab launchers throughout the app. */ diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt index 4949c955bc..cb94bc0fdf 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendCustomDateChooser.kt @@ -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 diff --git a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt index e48613e295..cbd5d73f9a 100644 --- a/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt +++ b/app/src/main/kotlin/com/x8bit/bitwarden/ui/tools/feature/send/addedit/components/AddEditSendDeletionDateChooser.kt @@ -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 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 new file mode 100644 index 0000000000..24af126d8c --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dialog/BitwardenDatePickerDialog.kt @@ -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 = {}, + ) + } +} 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 new file mode 100644 index 0000000000..1bbb5a8ffd --- /dev/null +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/dropdown/BitwardenDatePickerButton.kt @@ -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, + ) + } +} diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt index 856827b3e9..1f72dd66ce 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/components/field/color/BitwardenTextFieldColors.kt @@ -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, diff --git a/ui/src/main/kotlin/com/bitwarden/ui/platform/composition/LocalProviders.kt b/ui/src/main/kotlin/com/bitwarden/ui/platform/composition/LocalProviders.kt index baa62f081f..4b8ed99c68 100644 --- a/ui/src/main/kotlin/com/bitwarden/ui/platform/composition/LocalProviders.kt +++ b/ui/src/main/kotlin/com/bitwarden/ui/platform/composition/LocalProviders.kt @@ -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 = error("CompositionLocal LocalCardTextAnalyzer not present") } +/** + * Provides access to the clock throughout the app. + */ +val LocalClock: ProvidableCompositionLocal = compositionLocalOf { Clock.systemDefaultZone() } + /** * Provides access to the QR Code Analyzer throughout the app. */