diff --git a/README.md b/README.md index 01909e3..c708146 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/library/api/library.api b/library/api/library.api index 2ecfd26..2d55f3a 100644 --- a/library/api/library.api +++ b/library/api/library.api @@ -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 (Lkotlin/jvm/functions/Function1;Ljava/util/List;Lio/github/boguszpawlowski/composecalendar/selection/SelectionMode;)V + public synthetic fun (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 diff --git a/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt b/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt index 3f1942d..a3fe559 100644 --- a/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt +++ b/library/src/main/java/io/github/boguszpawlowski/composecalendar/Calendar.kt @@ -218,23 +218,21 @@ public fun 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 = emptyList(), initialSelectionMode: SelectionMode = SelectionMode.Single, - onSelectionChanged: (List) -> Unit = {}, + confirmSelectionChange: (newValue: List) -> 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 = remember { CalendarState(monthState, selectionState) } diff --git a/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt b/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt index fcb6d5f..821e006 100644 --- a/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt +++ b/library/src/main/java/io/github/boguszpawlowski/composecalendar/selection/SelectionState.kt @@ -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) -> Unit, + private val confirmSelectionChange: (newValue: List) -> Boolean = { true }, selection: List, selectionMode: SelectionMode, ) : SelectionState { @@ -32,9 +33,8 @@ public class DynamicSelectionState( public var selection: List get() = _selection set(value) { - if (value != selection) { + if (value != selection && confirmSelectionChange(value)) { _selection = value - onSelectionChanged(value) } } @@ -55,14 +55,16 @@ public class DynamicSelectionState( internal companion object { @Suppress("FunctionName", "UNCHECKED_CAST") // Factory function - fun Saver(onSelectionChanged: (List) -> Unit): Saver = + fun Saver( + confirmSelectionChange: (newValue: List) -> Boolean, + ): Saver = 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)?.map { LocalDate.parse(it) }.orEmpty(), ) diff --git a/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt b/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt index fbe25d4..75ca49c 100644 --- a/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt +++ b/library/src/test/java/io/github/boguszpawlowski/composecalendar/selection/SelectionStateTest.kt @@ -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 @@ -17,7 +18,7 @@ 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()) @@ -25,7 +26,7 @@ internal class SelectionStateTest : ShouldSpec({ } 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) @@ -36,7 +37,7 @@ 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) @@ -44,7 +45,7 @@ internal class SelectionStateTest : ShouldSpec({ } 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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) @@ -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) + } + } }) diff --git a/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt b/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt index 596e651..bdbc3be 100644 --- a/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt +++ b/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/KotlinDateTimeSample.kt @@ -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(), diff --git a/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt b/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt index 87b062d..2766c2a 100644 --- a/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt +++ b/sample/src/main/java/io/github/boguszpawlowski/composecalendar/sample/ViewModelSample.kt @@ -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, )