diff --git a/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePicker.kt b/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePicker.kt index 42e598e9..9fbf0597 100644 --- a/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePicker.kt +++ b/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePicker.kt @@ -1,21 +1,23 @@ package com.spendesk.grapes.compose.calendar +import androidx.compose.foundation.layout.padding import androidx.compose.material3.DatePicker -import androidx.compose.material3.DatePickerDefaults +import androidx.compose.material3.DatePickerColors import androidx.compose.material3.DisplayMode import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SelectableDates +import androidx.compose.material3.Text import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.spendesk.grapes.compose.extensions.resetDateToMidnight -import com.spendesk.grapes.compose.extensions.resetDateToTomorrowMidnight import com.spendesk.grapes.compose.theme.GrapesTheme import java.util.Calendar import java.util.Date -import java.util.TimeZone /** * @author : dany @@ -26,116 +28,125 @@ import java.util.TimeZone * Grapes date picker which lets the user select a date via a calendar UI. * * @param modifier The [Modifier] to be applied to this date picker - * @param date The pre-selected date in the calendar. Defaults to now if not provided - * @param minDate The minimum selectable date in the calendar (previous dates will be disabled) if provided. Defaults to infinite in the past - * @param maxDate The maximum selectable date in the calendar (further dates will be disabled) if provided. Defaults to infinite in the future + * @param initialDisplayedDate The pre-selected date in the calendar. Defaults to now if not provided + * @param yearRange The range of years to be displayed in the year picker. Defaults to 1900-2100 + * @param dateEdges The minimum and maximum selectable dates in the calendar. Defaults to infinite in the past and future + * @param colors The colors to be used for the date picker + * @param title The title to be displayed at the top of the date picker + * @param headline The headline to be displayed below the title of the date picker * @param onDateSelected Callback when a date is selected in the picker */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun GrapesDatePicker( modifier: Modifier = Modifier, - date: Date? = null, - minDate: Date? = null, - maxDate: Date? = null, - onDateSelected: ((Date) -> Unit)? = null + initialDisplayedDate: Date? = null, + yearRange: IntRange = GrapesDatePickerDefaults.YearRange, + dateEdges: SelectableDates = GrapesDatePickerDefaults.selectableDatesEdges(), + colors: DatePickerColors = GrapesDatePickerDefaults.colors(), + title: (@Composable () -> Unit)? = null, + headline: (@Composable () -> Unit)? = null, + onDateSelected: ((Date) -> Unit)? = null, ) { val selectedDate = rememberDatePickerState( - initialSelectedDateMillis = date?.time, - initialDisplayedMonthMillis = date?.time, - yearRange = DatePickerDefaults.YearRange, + initialSelectedDateMillis = initialDisplayedDate?.time, + initialDisplayedMonthMillis = initialDisplayedDate?.time, + yearRange = yearRange, initialDisplayMode = DisplayMode.Picker, - selectableDates = object : SelectableDates { - override fun isSelectableDate(utcTimeMillis: Long): Boolean { - val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { timeInMillis = utcTimeMillis } - - val isAfterMinDate = minDate?.let { calendar.time.after(it.resetDateToMidnight()) } ?: true - val isBeforeMaxDate = maxDate?.let { calendar.time.before(it.resetDateToTomorrowMidnight()) } ?: true - - return isAfterMinDate && isBeforeMaxDate - } - } + selectableDates = dateEdges, ) DatePicker( state = selectedDate, modifier = modifier, showModeToggle = false, - title = null, - headline = null, - colors = DatePickerDefaults.colors( - containerColor = GrapesTheme.colors.mainBackground, - titleContentColor = GrapesTheme.colors.mainNeutralDarkest, - headlineContentColor = GrapesTheme.colors.mainNeutralDarkest, - weekdayContentColor = GrapesTheme.colors.mainNeutralDarkest, - subheadContentColor = GrapesTheme.colors.mainNeutralDarkest, - yearContentColor = GrapesTheme.colors.mainNeutralDarkest, - currentYearContentColor = GrapesTheme.colors.mainNeutralDarkest, - selectedYearContentColor = GrapesTheme.colors.mainWhite, - selectedYearContainerColor = GrapesTheme.colors.mainPrimaryNormal, - dayContentColor = GrapesTheme.colors.mainNeutralDarkest, - selectedDayContentColor = GrapesTheme.colors.mainWhite, - selectedDayContainerColor = GrapesTheme.colors.mainPrimaryNormal, - todayContentColor = GrapesTheme.colors.mainPrimaryNormal, - todayDateBorderColor = GrapesTheme.colors.mainPrimaryNormal - ), + title = title, + headline = headline, + colors = colors, ) LaunchedEffect(selectedDate.selectedDateMillis) { selectedDate.selectedDateMillis?.let { - if (Date(it).resetDateToMidnight() != date?.resetDateToMidnight()) { + if (Date(it).resetDateToMidnight() != initialDisplayedDate?.resetDateToMidnight()) { onDateSelected?.invoke(Date(it)) } } } } -@Preview(showBackground = true) -@Composable -private fun GrapesDatePickerWithMinDateAndMaxDatePreview() { - val minDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_WEEK, -1) }.time - val maxDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_WEEK, 2) }.time +@OptIn(ExperimentalMaterial3Api::class) +private data class GrapesDatePickerData( + val title: String? = null, + val headline: String? = null, + val initialDisplayedDate: Date? = null, + val yearRange: IntRange? = null, + val dateEdges: SelectableDates? = null, + val onDateSelected: ((Date) -> Unit)? = null, +) - GrapesTheme { - GrapesDatePicker( - minDate = minDate, - maxDate = maxDate, - onDateSelected = { date -> println("Selected date: $date") } - ) - } -} +@OptIn(ExperimentalMaterial3Api::class) +private class GrapesDatePickerProvider : PreviewParameterProvider { -@Preview(showBackground = true) -@Composable -private fun GrapesDatePickerWithMinDatePreview() { - val minDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_WEEK, -1) }.time + private val calendar = Calendar.getInstance() - GrapesTheme { - GrapesDatePicker( - minDate = minDate, - onDateSelected = { date -> println("Selected date: $date") } - ) - } -} + override val values: Sequence = sequenceOf( + GrapesDatePickerData( + title = "Select a date", + headline = "Choose a date to proceed", + ), -@Preview(showBackground = true) -@Composable -private fun GrapesDatePickerWithMaxDatePreview() { - val maxDate = Calendar.getInstance().apply { add(Calendar.DAY_OF_WEEK, 2) }.time + GrapesDatePickerData( + initialDisplayedDate = calendar.time, // Today + dateEdges = GrapesDatePickerDefaults.selectableDatesEdges( + minDate = calendar.apply { add(Calendar.DAY_OF_WEEK, -1) }.time, + maxDate = calendar.apply { add(Calendar.DAY_OF_WEEK, 2) }.time + ), + ), - GrapesTheme { - GrapesDatePicker( - maxDate = maxDate, - onDateSelected = { date -> println("Selected date: $date") } + GrapesDatePickerData( + dateEdges = GrapesDatePickerDefaults.selectableDatesEdges( + minDate = calendar.apply { add(Calendar.DAY_OF_WEEK, -1) }.time, + maxDate = null + ), + ), + + GrapesDatePickerData( + dateEdges = GrapesDatePickerDefaults.selectableDatesEdges( + minDate = null, + maxDate = calendar.apply { add(Calendar.DAY_OF_WEEK, 1) }.time + ), ) - } + ) } -@Preview(showBackground = true) +@OptIn(ExperimentalMaterial3Api::class) @Composable -private fun GrapesDatePickerPreview() { +@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true) +private fun GrapesDatePickerPreview( + @PreviewParameter(GrapesDatePickerProvider::class) data: GrapesDatePickerData, +) { GrapesTheme { GrapesDatePicker( + title = data.title?.let { + @Composable { + Text( + modifier = Modifier.padding(GrapesTheme.dimensions.spacing3), + text = it, + style = GrapesTheme.typography.titleXl + ) + } + }, + headline = data.headline?.let { + @Composable { + Text( + modifier = Modifier.padding(GrapesTheme.dimensions.spacing3), + text = it, + style = GrapesTheme.typography.titleM + ) + } + }, + initialDisplayedDate = data.initialDisplayedDate, + dateEdges = data.dateEdges ?: GrapesDatePickerDefaults.selectableDatesEdges(), onDateSelected = { date -> println("Selected date: $date") } ) } diff --git a/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePickerDefaults.kt b/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePickerDefaults.kt new file mode 100644 index 00000000..43ad90ea --- /dev/null +++ b/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePickerDefaults.kt @@ -0,0 +1,73 @@ +package com.spendesk.grapes.compose.calendar + +import androidx.compose.material3.DatePickerColors +import androidx.compose.material3.DatePickerDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SelectableDates +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import com.spendesk.grapes.compose.extensions.resetDateToMidnight +import com.spendesk.grapes.compose.extensions.resetDateToTomorrowMidnight +import com.spendesk.grapes.compose.theme.GrapesTheme +import java.util.Calendar +import java.util.Date +import java.util.TimeZone + +@Immutable +object GrapesDatePickerDefaults { + + val YearRange: IntRange = IntRange(1900, 2100) + + @ExperimentalMaterial3Api + @Composable + fun colors( + containerColor: Color = GrapesTheme.colors.structureBackground, + titleContentColor: Color = GrapesTheme.colors.neutralDarker, + headlineContentColor: Color = GrapesTheme.colors.neutralDarker, + weekdayContentColor: Color = GrapesTheme.colors.neutralDarker, + subheadContentColor: Color = GrapesTheme.colors.neutralDarker, + yearContentColor: Color = GrapesTheme.colors.neutralDarker, + currentYearContentColor: Color = GrapesTheme.colors.neutralDarker, + selectedYearContentColor: Color = Color.White, + selectedYearContainerColor: Color = GrapesTheme.colors.primaryNormal, + dayContentColor: Color = GrapesTheme.colors.neutralDarker, + selectedDayContentColor: Color = Color.White, + selectedDayContainerColor: Color = GrapesTheme.colors.primaryNormal, + todayContentColor: Color = GrapesTheme.colors.primaryNormal, + todayDateBorderColor: Color = GrapesTheme.colors.primaryNormal, + ): DatePickerColors = DatePickerDefaults.colors( + containerColor = containerColor, + titleContentColor = titleContentColor, + headlineContentColor = headlineContentColor, + weekdayContentColor = weekdayContentColor, + subheadContentColor = subheadContentColor, + yearContentColor = yearContentColor, + currentYearContentColor = currentYearContentColor, + selectedYearContentColor = selectedYearContentColor, + selectedYearContainerColor = selectedYearContainerColor, + dayContentColor = dayContentColor, + selectedDayContentColor = selectedDayContentColor, + selectedDayContainerColor = selectedDayContainerColor, + todayContentColor = todayContentColor, + todayDateBorderColor = todayDateBorderColor, + ) + + @OptIn(ExperimentalMaterial3Api::class) + fun selectableDatesEdges(minDate: Date? = null, maxDate: Date? = null): SelectableDates { + return if (minDate != null || maxDate != null) { + object : SelectableDates { + override fun isSelectableDate(utcTimeMillis: Long): Boolean { + val calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply { timeInMillis = utcTimeMillis } + + val isAfterMinDate = minDate?.let { calendar.time.after(it.resetDateToMidnight()) } ?: true + val isBeforeMaxDate = maxDate?.let { calendar.time.before(it.resetDateToTomorrowMidnight()) } ?: true + + return isAfterMinDate && isBeforeMaxDate + } + } + } else { + object : SelectableDates {} + } + } +} diff --git a/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePickerDialog.kt b/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePickerDialog.kt new file mode 100644 index 00000000..5b7cc8b6 --- /dev/null +++ b/library-compose/src/main/java/com/spendesk/grapes/compose/calendar/GrapesDatePickerDialog.kt @@ -0,0 +1,58 @@ +package com.spendesk.grapes.compose.calendar + +import androidx.compose.material3.DatePickerColors +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.DialogProperties +import com.spendesk.grapes.compose.theme.GrapesTheme +import java.util.Date + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GrapesDatePickerDialog( + initialDisplayedDate: Date, + onDateSelected: (selectedDate: Date) -> Unit, + modifier: Modifier = Modifier, + colors: DatePickerColors = GrapesDatePickerDefaults.colors(), + shape: Shape = GrapesTheme.shapes.shape2, + dismissOnBack: Boolean = true, + dismissOnClickOutside: Boolean = true, + confirmButton: @Composable () -> Unit = { }, + dismissButton: (@Composable () -> Unit)? = null, + onDismissRequest: (() -> Unit)? = null, +) { + DatePickerDialog( + modifier = modifier, + onDismissRequest = { onDismissRequest?.invoke() }, + confirmButton = confirmButton, + dismissButton = dismissButton, + colors = colors, + shape = shape, + properties = DialogProperties( + dismissOnBackPress = dismissOnBack, + dismissOnClickOutside = dismissOnClickOutside, + ), + content = { + GrapesDatePicker( + initialDisplayedDate = initialDisplayedDate, + onDateSelected = { date -> onDateSelected(date) } + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview(showBackground = false) +@Composable +fun PreviewGrapesDatePickerDialog() { + GrapesTheme { + GrapesDatePickerDialog( + initialDisplayedDate = Date(), + onDateSelected = { } + ) + } +}