Skip to content

Commit 9ea7c26

Browse files
committed
(Calendar) Setting the initial week #80
1 parent 547fdc7 commit 9ea7c26

File tree

4 files changed

+171
-31
lines changed

4 files changed

+171
-31
lines changed

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

Lines changed: 132 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.maxkeppeler.sheets.calendar.functional
1919

2020
import android.util.Range
2121
import androidx.compose.material3.ExperimentalMaterial3Api
22+
import androidx.compose.ui.test.assertIsDisplayed
2223
import androidx.compose.ui.test.assertIsNotEnabled
2324
import androidx.compose.ui.test.junit4.createComposeRule
2425
import androidx.compose.ui.test.performClick
@@ -45,7 +46,7 @@ class CalendarViewTests {
4546
val rule = createComposeRule()
4647

4748
@Test
48-
fun calendarViewDateSelectionSuccess() {
49+
fun givenCalendarView_whenDateSelected_thenDateSelectionSuccess() {
4950
val testDate = LocalDate.now()
5051
.withDayOfMonth(12)
5152

@@ -65,7 +66,7 @@ class CalendarViewTests {
6566
}
6667

6768
@Test
68-
fun calendarViewDateSelectionInvalid() {
69+
fun givenCalendarView_whenNoDateSelected_thenDateSelectionInvalid() {
6970
rule.setContentAndWaitForIdle {
7071
CalendarView(
7172
useCaseState = UseCaseState(visible = true),
@@ -76,7 +77,7 @@ class CalendarViewTests {
7677
}
7778

7879
@Test
79-
fun calendarViewDatesSelectionSuccess() {
80+
fun givenCalendarView_whenMultipleDatesSelected_thenDatesSelectionSuccess() {
8081
val testDates = listOf(
8182
LocalDate.now().withDayOfMonth(2),
8283
LocalDate.now().withDayOfMonth(8),
@@ -104,7 +105,7 @@ class CalendarViewTests {
104105
}
105106

106107
@Test
107-
fun calendarViewDatesSelectionInvalid() {
108+
fun givenCalendarView_whenNoDatesSelected_thenDatesSelectionInvalid() {
108109
rule.setContentAndWaitForIdle {
109110
CalendarView(
110111
useCaseState = UseCaseState(visible = true),
@@ -115,7 +116,7 @@ class CalendarViewTests {
115116
}
116117

117118
@Test
118-
fun calendarViewPeriodSelectionSuccess() {
119+
fun givenCalendarView_whenDateSelectedWithStyleMonthAndDisabledDates_thenDateSelectionStyleMonthConfigDatesDisabled() {
119120
val testStartDate = LocalDate.now().withDayOfMonth(2)
120121
val testEndDate = LocalDate.now().withDayOfMonth(12)
121122

@@ -147,7 +148,7 @@ class CalendarViewTests {
147148
}
148149

149150
@Test
150-
fun calendarViewDateSelectionStyleMonthConfigDatesDisabled() {
151+
fun givenCalendarView_whenMultipleDatesSelectedWithStyleMonthAndDisabledDates_thenDatesSelectionStyleMonthConfigDatesDisabled() {
151152
val testDate = LocalDate.now().withDayOfMonth(15)
152153
val newDates = listOf(
153154
testDate.plusDays(2),
@@ -183,7 +184,7 @@ class CalendarViewTests {
183184
}
184185

185186
@Test
186-
fun calendarViewDatesSelectionStyleMonthConfigDatesDisabled() {
187+
fun givenCalendarView_whenPeriodSelectedWithStyleMonthAndDisabledDates_thenPeriodSelectionStyleMonthConfigDatesDisabled() {
187188
val testDate = LocalDate.now().withDayOfMonth(15)
188189
val defaultDates = listOf(
189190
testDate.minusDays(10),
@@ -224,7 +225,7 @@ class CalendarViewTests {
224225

225226

226227
@Test
227-
fun calendarViewPeriodSelectionStyleMonthConfigDatesDisabled() {
228+
fun givenCalendarView_whenPeriodSelectedWithStyleMonthAndDisabledDatesAlt_thenPeriodSelectionStyleMonthConfigDatesDisabled() {
228229
val testDate = LocalDate.now().withDayOfMonth(15)
229230
val disabledDates = listOf(
230231
testDate.minusDays(1),
@@ -262,7 +263,7 @@ class CalendarViewTests {
262263
}
263264

264265
@Test
265-
fun calendarViewPeriodSelectionInvalid() {
266+
fun givenCalendarView_whenNoPeriodSelected_thenPeriodSelectionInvalid() {
266267
rule.setContentAndWaitForIdle {
267268
CalendarView(
268269
useCaseState = UseCaseState(visible = true),
@@ -273,7 +274,7 @@ class CalendarViewTests {
273274
}
274275

275276
@Test
276-
fun calendarViewPeriodSelectionInvalidSelectEndDateBeforeStartDate() {
277+
fun givenCalendarView_whenEndDateSelectedBeforeStartDate_thenPeriodSelectionInvalidSelectEndDateBeforeStartDate() {
277278
val testStartDate = LocalDate.now().withDayOfMonth(12)
278279
val testEndDate = LocalDate.now().withDayOfMonth(2)
279280
rule.setContentAndWaitForIdle {
@@ -297,7 +298,7 @@ class CalendarViewTests {
297298
}
298299

299300
@Test
300-
fun calendarViewDisplaysCalendarStyleWeek() {
301+
fun givenCalendarView_whenCalendarStyleWeek_thenCalendarViewDisplaysCalendarStyleWeek() {
301302
rule.setContentAndWaitForIdle {
302303
CalendarView(
303304
useCaseState = UseCaseState(visible = true),
@@ -309,7 +310,7 @@ class CalendarViewTests {
309310
}
310311

311312
@Test
312-
fun calendarViewDisplaysCalendarStyleMonth() {
313+
fun givenCalendarView_whenCalendarStyleMonth_thenCalendarViewDisplaysCalendarStyleMonth() {
313314
rule.setContentAndWaitForIdle {
314315
CalendarView(
315316
useCaseState = UseCaseState(visible = true),
@@ -320,4 +321,123 @@ class CalendarViewTests {
320321
rule.onPositiveButton().assertIsNotEnabled()
321322
}
322323

324+
325+
@Test
326+
fun givenCalendarView_whenDateSelectedWithCameraDate_thenDisplayCorrectTime() {
327+
val testDate = LocalDate.now().withDayOfMonth(15)
328+
val testCameraDate = LocalDate.now().minusMonths(2)
329+
rule.setContentAndWaitForIdle {
330+
CalendarView(
331+
useCaseState = UseCaseState(visible = true),
332+
selection = CalendarSelection.Date(
333+
selectedDate = testDate,
334+
onSelectDate = { date -> }
335+
),
336+
config = CalendarConfig(
337+
style = CalendarStyle.MONTH,
338+
cameraDate = testCameraDate
339+
)
340+
)
341+
}
342+
343+
rule.onNodeWithTags(
344+
TestTags.CALENDAR_DATE_SELECTION,
345+
testCameraDate.format(DateTimeFormatter.ISO_DATE)
346+
).apply {
347+
assertExists()
348+
assertIsDisplayed()
349+
}
350+
}
351+
352+
@Test
353+
fun givenCalendarView_whenDateSelectedWithCameraDateOutsideBoundary_thenDisplaySelectedTime() {
354+
val testDate = LocalDate.now().withDayOfMonth(15)
355+
val testBoundary = testDate.minusYears(2)..testDate.plusYears(2)
356+
val testCameraDate = LocalDate.now().minusYears(4)
357+
rule.setContentAndWaitForIdle {
358+
CalendarView(
359+
useCaseState = UseCaseState(visible = true),
360+
selection = CalendarSelection.Date(
361+
selectedDate = testDate,
362+
onSelectDate = { date -> }
363+
),
364+
config = CalendarConfig(
365+
boundary = testBoundary,
366+
cameraDate = testCameraDate,
367+
style = CalendarStyle.MONTH
368+
)
369+
)
370+
}
371+
372+
rule.onNodeWithTags(
373+
TestTags.CALENDAR_DATE_SELECTION,
374+
testCameraDate.format(DateTimeFormatter.ISO_DATE)
375+
).assertDoesNotExist()
376+
377+
rule.onNodeWithTags(
378+
TestTags.CALENDAR_DATE_SELECTION,
379+
testDate.format(DateTimeFormatter.ISO_DATE)
380+
).apply {
381+
assertExists()
382+
assertIsDisplayed()
383+
}
384+
}
385+
386+
387+
@Test
388+
fun givenCalendarView_whenCameraDateOutsideBoundaryCurrentTimeInsideBoundary_thenDisplayCurrentTime() {
389+
val testDate = LocalDate.now()
390+
val testBoundary = testDate.minusYears(2)..testDate.plusYears(2)
391+
val testCameraDate = LocalDate.now().minusYears(4)
392+
rule.setContentAndWaitForIdle {
393+
CalendarView(
394+
useCaseState = UseCaseState(visible = true),
395+
selection = CalendarSelection.Date(
396+
onSelectDate = { date -> }
397+
),
398+
config = CalendarConfig(
399+
boundary = testBoundary,
400+
cameraDate = testCameraDate,
401+
style = CalendarStyle.MONTH
402+
)
403+
)
404+
}
405+
406+
rule.onNodeWithTags(
407+
TestTags.CALENDAR_DATE_SELECTION,
408+
testDate.format(DateTimeFormatter.ISO_DATE)
409+
).apply {
410+
assertExists()
411+
assertIsDisplayed()
412+
}
413+
}
414+
415+
@Test
416+
fun givenCalendarView_whenCameraDateOutsideBoundaryCurrentTimeOutsideBoundary_thenDisplayCurrentTime() {
417+
val testDate = LocalDate.now()
418+
val testBoundary = testDate.plusYears(2)..testDate.plusYears(4)
419+
val testCameraDate = LocalDate.now().minusYears(4)
420+
rule.setContentAndWaitForIdle {
421+
CalendarView(
422+
useCaseState = UseCaseState(visible = true),
423+
selection = CalendarSelection.Date(
424+
onSelectDate = { date -> }
425+
),
426+
config = CalendarConfig(
427+
boundary = testBoundary,
428+
cameraDate = testCameraDate,
429+
style = CalendarStyle.MONTH
430+
)
431+
)
432+
}
433+
434+
rule.onNodeWithTags(
435+
TestTags.CALENDAR_DATE_SELECTION,
436+
testBoundary.start.format(DateTimeFormatter.ISO_DATE)
437+
).apply {
438+
assertExists()
439+
assertIsDisplayed()
440+
}
441+
}
442+
323443
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import com.maxkeppeler.sheets.calendar.utils.endOfMonth
3838
import com.maxkeppeler.sheets.calendar.utils.endOfWeek
3939
import com.maxkeppeler.sheets.calendar.utils.endValue
4040
import com.maxkeppeler.sheets.calendar.utils.getInitialCameraDate
41+
import com.maxkeppeler.sheets.calendar.utils.getInitialCustomCameraDate
4142
import com.maxkeppeler.sheets.calendar.utils.jumpNext
4243
import com.maxkeppeler.sheets.calendar.utils.jumpPrev
4344
import com.maxkeppeler.sheets.calendar.utils.rangeValue
@@ -65,7 +66,9 @@ internal class CalendarState(
6566
val today by mutableStateOf(LocalDate.now())
6667
var mode by mutableStateOf(stateData?.mode ?: CalendarDisplayMode.CALENDAR)
6768
var cameraDate by mutableStateOf(
68-
stateData?.cameraDate ?: selection.getInitialCameraDate(config.boundary)
69+
stateData?.cameraDate
70+
?: getInitialCustomCameraDate(config.cameraDate, config.boundary)
71+
?: getInitialCameraDate(selection, config.boundary)
6972
)
7073
var date = mutableStateOf(stateData?.date ?: selection.dateValue)
7174
var dates = mutableStateListOf(*(stateData?.dates ?: selection.datesValue))

calendar/src/main/java/com/maxkeppeler/sheets/calendar/models/CalendarConfig.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ import com.maxkeppeker.sheets.core.models.base.BaseConfigs
2020
import com.maxkeppeker.sheets.core.utils.BaseConstants.DEFAULT_ICON_STYLE
2121
import com.maxkeppeler.sheets.calendar.utils.Constants
2222
import java.time.LocalDate
23-
import java.util.*
23+
import java.util.Locale
2424

2525
/**
2626
* The general configuration for the calendar dialog.
2727
* @param locale The locale of the calendar.
2828
* @param style The style of the calendar.
29+
* @param cameraDate The date that is initially displayed when the calendar is opened.
2930
* @param monthSelection Allow the direct selection of a month.
3031
* @param yearSelection Allow the direct selection of a year.
3132
* @param boundary The range of dates that are displayed.
@@ -35,6 +36,7 @@ import java.util.*
3536
class CalendarConfig(
3637
val locale: Locale = Locale.getDefault(),
3738
val style: CalendarStyle = CalendarStyle.MONTH,
39+
val cameraDate: LocalDate? = null,
3840
val monthSelection: Boolean = Constants.DEFAULT_MONTH_SELECTION,
3941
val yearSelection: Boolean = Constants.DEFAULT_YEAR_SELECTION,
4042
val boundary: ClosedRange<LocalDate> = Constants.DEFAULT_RANGE,

calendar/src/main/java/com/maxkeppeler/sheets/calendar/utils/Utils.kt

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,18 @@
1616
package com.maxkeppeler.sheets.calendar.utils
1717

1818
import androidx.annotation.RestrictTo
19-
import com.maxkeppeler.sheets.calendar.models.*
19+
import com.maxkeppeler.sheets.calendar.models.CalendarConfig
20+
import com.maxkeppeler.sheets.calendar.models.CalendarData
21+
import com.maxkeppeler.sheets.calendar.models.CalendarDateData
22+
import com.maxkeppeler.sheets.calendar.models.CalendarMonthData
23+
import com.maxkeppeler.sheets.calendar.models.CalendarSelection
24+
import com.maxkeppeler.sheets.calendar.models.CalendarStyle
2025
import java.time.DayOfWeek
2126
import java.time.LocalDate
2227
import java.time.Month
2328
import java.time.temporal.TemporalAdjusters
2429
import java.time.temporal.WeekFields
25-
import java.util.*
30+
import java.util.Locale
2631

2732
/**
2833
* Returns the week of the week-based-year for this [LocalDate].
@@ -95,9 +100,11 @@ internal val LocalDate.previousWeek: LocalDate
95100
get() = when {
96101
dayOfMonth == Constants.FIRST_DAY_IN_MONTH
97102
&& dayOfWeek != DayOfWeek.MONDAY -> with(DayOfWeek.MONDAY)
103+
98104
dayOfMonth >= Constants.DAYS_IN_WEEK ||
99105
dayOfMonth == Constants.FIRST_DAY_IN_MONTH
100106
&& dayOfWeek == DayOfWeek.MONDAY -> minusWeeks(1)
107+
101108
else -> withDayOfMonth(Constants.FIRST_DAY_IN_MONTH)
102109
}
103110

@@ -150,27 +157,34 @@ fun LocalDate.jumpNext(config: CalendarConfig): LocalDate = when (config.style)
150157

151158
/**
152159
* Returns the initial date to be displayed on the CalendarView based on the selection mode.
153-
*
154-
* The initial camera date is calculated based on the selected mode. If the mode is [CalendarSelection.Date],
155-
* the selected date is returned. If the mode is [CalendarSelection.Dates], the first selected date is returned.
156-
* If the mode is [CalendarSelection.Period], the lower range of the selected period is returned.
157-
*
158-
* If the selected mode doesn't have a date, the current date will be returned as the initial camera date.
159-
*
160-
* @return The initial camera date.
160+
* @param selection The selection mode.
161+
* @param boundary The boundary of the calendar.
162+
* @return The initial date to be displayed on the CalendarView.
161163
*/
162-
internal fun CalendarSelection.getInitialCameraDate(boundary: ClosedRange<LocalDate>): LocalDate {
163-
val cameraDateBasedOnMode = when (this) {
164-
is CalendarSelection.Date -> selectedDate
165-
is CalendarSelection.Dates -> selectedDates?.firstOrNull()
166-
is CalendarSelection.Period -> selectedRange?.lower
167-
} ?: kotlin.run {
164+
internal fun getInitialCameraDate(selection: CalendarSelection, boundary: ClosedRange<LocalDate>): LocalDate {
165+
val cameraDateBasedOnMode = when (selection) {
166+
is CalendarSelection.Date -> selection.selectedDate
167+
is CalendarSelection.Dates -> selection.selectedDates?.firstOrNull()
168+
is CalendarSelection.Period -> selection.selectedRange?.lower
169+
} ?: run {
168170
val now = LocalDate.now()
169-
if (now in boundary) now else boundary.endInclusive
171+
if (now in boundary) now else boundary.start
170172
}
171173
return cameraDateBasedOnMode.startOfWeekOrMonth
172174
}
173175

176+
/**
177+
* Returns the custom initial date in case the camera date is within the boundary. Otherwise, it returns null.
178+
*
179+
* @param cameraDate The initial camera date.
180+
* @param boundary The boundary of the calendar.
181+
* @return The initial camera date if it's within the boundary, otherwise null.
182+
*/
183+
internal fun getInitialCustomCameraDate(
184+
cameraDate: LocalDate?,
185+
boundary: ClosedRange<LocalDate>
186+
): LocalDate? = cameraDate?.takeIf { it in boundary }?.startOfWeekOrMonth
187+
174188
/**
175189
* Get selection value of date.
176190
*/
@@ -300,6 +314,7 @@ internal fun calcCalendarDateData(
300314
is CalendarSelection.Dates -> {
301315
selectedDates?.contains(date) ?: false
302316
}
317+
303318
is CalendarSelection.Period -> {
304319
val selectedStart = selectedRange.first == date
305320
selectedStartInit = selectedStart && selectedRange.second != null

0 commit comments

Comments
 (0)