Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(compose) Rework GrapesDatePicker + Add GrapesDatePickerDialog #344

Merged
merged 3 commits into from
Mar 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<GrapesDatePickerData> {

@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<GrapesDatePickerData> = 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") }
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {}
}
}
}
Original file line number Diff line number Diff line change
@@ -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 = { }
)
}
}
Loading