Skip to content

Commit 5637c2c

Browse files
committed
(Calendar) Respect disabled dates when selection date range #75
1 parent 5b0d0bb commit 5637c2c

File tree

2 files changed

+134
-5
lines changed

2 files changed

+134
-5
lines changed

calendar/src/androidTest/java/com/maxkeppeler/sheets/calendar/functional/CalendarViewTests.kt

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
package com.maxkeppeler.sheets.calendar.functional
1919

20+
import android.util.Range
2021
import androidx.compose.material3.ExperimentalMaterial3Api
2122
import androidx.compose.ui.test.assertIsNotEnabled
2223
import androidx.compose.ui.test.junit4.createComposeRule
@@ -145,6 +146,121 @@ class CalendarViewTests {
145146
assert(endDate == testEndDate)
146147
}
147148

149+
@Test
150+
fun calendarViewDateSelectionStyleMonthConfigDatesDisabled() {
151+
val testDate = LocalDate.now().withDayOfMonth(15)
152+
val newDates = listOf(
153+
testDate.plusDays(2),
154+
testDate,
155+
testDate.minusDays(3)
156+
)
157+
val disabledDates = listOf(
158+
testDate.minusDays(3),
159+
)
160+
var selectedDate: LocalDate? = null
161+
rule.setContentAndWaitForIdle {
162+
CalendarView(
163+
useCaseState = UseCaseState(visible = true),
164+
selection = CalendarSelection.Date(
165+
onSelectDate = { dates -> selectedDate = dates }
166+
),
167+
config = CalendarConfig(
168+
style = CalendarStyle.MONTH,
169+
disabledDates = disabledDates
170+
)
171+
)
172+
}
173+
174+
newDates.forEach { date ->
175+
rule.onNodeWithTags(
176+
TestTags.CALENDAR_DATE_SELECTION,
177+
date.format(DateTimeFormatter.ISO_DATE)
178+
).performClick()
179+
}
180+
181+
rule.onPositiveButton().performClick()
182+
assert(selectedDate == testDate)
183+
}
184+
185+
@Test
186+
fun calendarViewDatesSelectionStyleMonthConfigDatesDisabled() {
187+
val testDate = LocalDate.now().withDayOfMonth(15)
188+
val defaultDates = listOf(
189+
testDate.minusDays(10),
190+
testDate.minusDays(5)
191+
)
192+
val newDates = listOf(
193+
testDate,
194+
testDate.minusDays(3)
195+
)
196+
val disabledDates = listOf(
197+
testDate.minusDays(3),
198+
)
199+
var selectedDates: List<LocalDate>? = null
200+
rule.setContentAndWaitForIdle {
201+
CalendarView(
202+
useCaseState = UseCaseState(visible = true),
203+
selection = CalendarSelection.Dates(
204+
selectedDates = defaultDates,
205+
onSelectDates = { dates -> selectedDates = dates }
206+
),
207+
config = CalendarConfig(
208+
style = CalendarStyle.MONTH,
209+
disabledDates = disabledDates
210+
)
211+
)
212+
}
213+
214+
newDates.forEach { date ->
215+
rule.onNodeWithTags(
216+
TestTags.CALENDAR_DATE_SELECTION,
217+
date.format(DateTimeFormatter.ISO_DATE)
218+
).performClick()
219+
}
220+
221+
rule.onPositiveButton().performClick()
222+
assert(selectedDates?.sorted() == (defaultDates + testDate).sorted())
223+
}
224+
225+
226+
@Test
227+
fun calendarViewPeriodSelectionStyleMonthConfigDatesDisabled() {
228+
val testDate = LocalDate.now().withDayOfMonth(15)
229+
val disabledDates = listOf(
230+
testDate.minusDays(1),
231+
testDate.minusDays(2),
232+
testDate.minusDays(3),
233+
)
234+
val testStartDate = testDate.minusDays(4)
235+
val testEndDate = testDate.plusDays(2)
236+
var selectedDate: Range<LocalDate>? = null
237+
rule.setContentAndWaitForIdle {
238+
CalendarView(
239+
useCaseState = UseCaseState(visible = true),
240+
selection = CalendarSelection.Period { startDate, endDate ->
241+
selectedDate = Range(startDate, endDate)
242+
},
243+
config = CalendarConfig(
244+
style = CalendarStyle.MONTH,
245+
disabledDates = disabledDates
246+
)
247+
)
248+
}
249+
250+
rule.onNodeWithTags(
251+
TestTags.CALENDAR_DATE_SELECTION,
252+
testStartDate.format(DateTimeFormatter.ISO_DATE)
253+
).performClick()
254+
255+
rule.onNodeWithTags(
256+
TestTags.CALENDAR_DATE_SELECTION,
257+
testEndDate.format(DateTimeFormatter.ISO_DATE)
258+
).performClick()
259+
260+
rule.onPositiveButton().assertIsNotEnabled()
261+
assert(selectedDate == null)
262+
}
263+
148264
@Test
149265
fun calendarViewPeriodSelectionInvalid() {
150266
rule.setContentAndWaitForIdle {

calendar/src/main/java/com/maxkeppeler/sheets/calendar/CalendarState.kt

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,16 +245,29 @@ internal class CalendarState(
245245
}
246246

247247
is CalendarSelection.Period -> {
248-
val beforeStart =
249-
range.startValue?.let { newDate.isBefore(it) } ?: false
250-
val containsDisabledDate = range.endValue?.let { startDate ->
251-
config.disabledDates?.any { it.isAfter(startDate) && it.isBefore(newDate) }
248+
// Check if the selected range includes any disabled dates
249+
val includesDisabledDate = range.startValue?.let { startDate ->
250+
config.disabledDates?.any { disabledDate ->
251+
disabledDate.isAfter(startDate) && disabledDate.isBefore(newDate) || disabledDate == newDate
252+
}
252253
} ?: false
253-
if (isRangeSelectionStart || beforeStart || containsDisabledDate) {
254+
255+
// Reset the range if the selection includes a disabled date
256+
if (includesDisabledDate) {
257+
range[Constants.RANGE_START] = newDate
258+
range[Constants.RANGE_END] = null
259+
return
260+
}
261+
262+
// Check if the selection is the start or the date is before the start
263+
val beforeStart = range.startValue?.let { newDate.isBefore(it) } ?: false
264+
if (isRangeSelectionStart || beforeStart) {
265+
// Reset the range if the selection includes a disabled date
254266
range[Constants.RANGE_START] = newDate
255267
range[Constants.RANGE_END] = null
256268
isRangeSelectionStart = false
257269
} else {
270+
// Check if the selection is the end or the date is after the end
258271
range[Constants.RANGE_END] = newDate
259272
isRangeSelectionStart = true
260273
}

0 commit comments

Comments
 (0)