Skip to content

Commit

Permalink
Add vetoing functionality to the selection state (#68)
Browse files Browse the repository at this point in the history
- Removed: `onSelectionChange` callback
- Added: `confirmSelectionChange` callback
  • Loading branch information
boguszpawlowski authored May 1, 2022
1 parent 1fb5cfc commit 269a63e
Show file tree
Hide file tree
Showing 7 changed files with 51 additions and 25 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ Selection modes are represented by `SelectionMode` enum, with following values:
- `Single` - only single day is selectable - selection will contain one or zero days selected.
- `Multiple` - a list of dates can be selected.
- `Period` - selectable period - implemented by `start` and `end` dates. - selection will contain all dates between start and the end date.
This implementation of SelectionState also allows for handling side-effects and vetoing the state change via `confirmSelectionChange` callback.

## KotlinX DateTime
As the core of the library is built on `java.time` library, on Android it requires to use [core libary desugaring](https://developer.android.com/studio/write/java8-support) to be able to access it's API.
Expand Down
1 change: 1 addition & 0 deletions library/api/library.api
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSe

public final class io/github/boguszpawlowski/composecalendar/selection/DynamicSelectionState : io/github/boguszpawlowski/composecalendar/selection/SelectionState {
public fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V
public synthetic fun <init> (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getSelection ()Ljava/util/List;
public final fun getSelectionMode ()Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;
public fun isDateSelected (Ljava/time/LocalDate;)Z
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,23 +218,21 @@ public fun <T : SelectionState> Calendar(
* @param initialMonth initially rendered month
* @param initialSelection initial selection of the composable
* @param initialSelectionMode initial mode of the selection
* @param onSelectionChanged callback for side effects triggered when the selection state changes
* @param confirmSelectionChange callback for optional side-effects handling and vetoing the state change
*/
@Composable
public fun rememberSelectableCalendarState(
initialMonth: YearMonth = YearMonth.now(),
initialSelection: List<LocalDate> = emptyList(),
initialSelectionMode: SelectionMode = SelectionMode.Single,
onSelectionChanged: (List<LocalDate>) -> Unit = {},
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
monthState: MonthState = rememberSaveable(saver = MonthState.Saver()) {
MonthState(initialMonth = initialMonth)
},
selectionState: DynamicSelectionState = rememberSaveable(
saver = DynamicSelectionState.Saver(
onSelectionChanged
)
saver = DynamicSelectionState.Saver(confirmSelectionChange),
) {
DynamicSelectionState(onSelectionChanged, initialSelection, initialSelectionMode)
DynamicSelectionState(confirmSelectionChange, initialSelection, initialSelectionMode)
},
): CalendarState<DynamicSelectionState> = remember { CalendarState(monthState, selectionState) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ public interface SelectionState {
/**
* Class that enables for dynamically changing selection modes in the runtime. Depending on the mode, selection changes differently.
* Mode can be varied by setting desired [SelectionMode] in the [selectionMode] mutable property.
* @param confirmSelectionChange return false from this callback to veto the selection change
*/
@Stable
public class DynamicSelectionState(
private val onSelectionChanged: (List<LocalDate>) -> Unit,
private val confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean = { true },
selection: List<LocalDate>,
selectionMode: SelectionMode,
) : SelectionState {
Expand All @@ -32,9 +33,8 @@ public class DynamicSelectionState(
public var selection: List<LocalDate>
get() = _selection
set(value) {
if (value != selection) {
if (value != selection && confirmSelectionChange(value)) {
_selection = value
onSelectionChanged(value)
}
}

Expand All @@ -55,14 +55,16 @@ public class DynamicSelectionState(

internal companion object {
@Suppress("FunctionName", "UNCHECKED_CAST") // Factory function
fun Saver(onSelectionChanged: (List<LocalDate>) -> Unit): Saver<DynamicSelectionState, Any> =
fun Saver(
confirmSelectionChange: (newValue: List<LocalDate>) -> Boolean,
): Saver<DynamicSelectionState, Any> =
listSaver(
save = { raw ->
listOf(raw.selectionMode, raw.selection.map { it.toString() })
},
restore = { restored ->
DynamicSelectionState(
onSelectionChanged = onSelectionChanged,
confirmSelectionChange = confirmSelectionChange,
selectionMode = restored[0] as SelectionMode,
selection = (restored[1] as? List<String>)?.map { LocalDate.parse(it) }.orEmpty(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package io.github.boguszpawlowski.composecalendar.selection

import io.github.boguszpawlowski.composecalendar.selection.SelectionMode.Multiple
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.collections.shouldContainExactly
Expand All @@ -17,15 +18,15 @@ internal class SelectionStateTest : ShouldSpec({

context("Selection state with SelectionMode.None") {
should("not change selection after new value arrives") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.None)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None)

state.onDateSelected(LocalDate.now())

state.selection shouldBe emptyList()
}

should("be able to change if mode has been changed") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.None)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.None)

state.selectionMode = SelectionMode.Single
state.onDateSelected(today)
Expand All @@ -36,15 +37,15 @@ internal class SelectionStateTest : ShouldSpec({

context("Selection state with SelectionMode.Single") {
should("change state to single after day is selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Single)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)

state.onDateSelected(today)

state.selection shouldBe listOf(today)
}

should("change state to none when same day is selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Single)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)

state.onDateSelected(today)
state.onDateSelected(today)
Expand All @@ -53,7 +54,7 @@ internal class SelectionStateTest : ShouldSpec({
}

should("change to other day when selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Single)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)

state.onDateSelected(today)
state.onDateSelected(tomorrow)
Expand All @@ -62,7 +63,7 @@ internal class SelectionStateTest : ShouldSpec({
}

should("not be mutable after selection mode is changed to None") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Single)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Single)

state.selectionMode = SelectionMode.None
state.onDateSelected(today)
Expand All @@ -73,7 +74,7 @@ internal class SelectionStateTest : ShouldSpec({

context("Selection state with SelectionMode.Multiple") {
should("allow for multiple days selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Multiple)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple)

state.onDateSelected(today)
state.onDateSelected(tomorrow)
Expand All @@ -85,7 +86,7 @@ internal class SelectionStateTest : ShouldSpec({
}

should("switch selection off once day is selected second time") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Multiple)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Multiple)

state.onDateSelected(today)
state.onDateSelected(tomorrow)
Expand All @@ -97,7 +98,7 @@ internal class SelectionStateTest : ShouldSpec({

context("Selection state with SelectionMode.Period") {
should("allow for period of days selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Period)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)

state.onDateSelected(today)
state.onDateSelected(tomorrow)
Expand All @@ -107,7 +108,7 @@ internal class SelectionStateTest : ShouldSpec({
}

should("switch selection off once start day is selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Period)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)

state.onDateSelected(today)
state.onDateSelected(tomorrow)
Expand All @@ -116,7 +117,7 @@ internal class SelectionStateTest : ShouldSpec({
state.selection shouldBe emptyList()
}
should("change end date once the date selected is between start and the end") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Period)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)

state.onDateSelected(yesterday)
state.onDateSelected(tomorrow)
Expand All @@ -126,7 +127,7 @@ internal class SelectionStateTest : ShouldSpec({
state.selection.last() shouldBe today
}
should("change start day once day before start is selected") {
val state = DynamicSelectionState({}, emptyList(), SelectionMode.Period)
val state = DynamicSelectionState({ true }, emptyList(), SelectionMode.Period)

state.onDateSelected(today)
state.onDateSelected(tomorrow)
Expand All @@ -150,4 +151,27 @@ internal class SelectionStateTest : ShouldSpec({
}
}
}
context("Selection State with confirm state change callback") {
var nextVetoResult = false
val initialSelection = LocalDate.of(1999, 10, 12)
val newSelection = initialSelection.plusDays(1)

val selectionState = DynamicSelectionState(
confirmSelectionChange = { nextVetoResult },
selection = listOf(initialSelection),
selectionMode = Multiple,
)
should("Not change the selection when change is vetoed") {
selectionState.onDateSelected(newSelection)

selectionState.selection shouldBe listOf(initialSelection)
}
should("Change the selection when change is not vetoed") {
nextVetoResult = true

selectionState.onDateSelected(newSelection)

selectionState.selection shouldBe listOf(initialSelection, newSelection)
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fun DateTimeCalendar(
) {
SelectableCalendar(
calendarState = rememberSelectableCalendarState(
onSelectionChanged = { selection -> onSelectionChanged(selection.map { it.toKotlinLocalDate() }) },
confirmSelectionChange = { selection -> onSelectionChanged(selection.map { it.toKotlinLocalDate() }); true },
initialSelectionMode = Multiple,
),
today = today.toJavaLocalDate(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fun ViewModelSample() {
val selectedPrice by viewModel.selectedRecipesPriceFlow.collectAsState(0)

val state = rememberSelectableCalendarState(
onSelectionChanged = viewModel::onSelectionChanged,
confirmSelectionChange = { viewModel.onSelectionChanged(it); true },
initialSelectionMode = Period,
)

Expand Down

0 comments on commit 269a63e

Please sign in to comment.