Skip to content

Commit 49c3d85

Browse files
committed
Refactor: Improve Date Picker UI, Input Mode, and Accessibility
This commit introduces several enhancements to the PersianDatePicker library, focusing on UI improvements, a new date input mode, and better accessibility. **Key Changes:** - **Date Input Mode:** - Added `PersianDateEnterSection` composable in `PersianDatePicker.kt` to allow users to input dates manually. - Implemented `DateVisualTransformation.kt` to format the input as `YYYY/MM/DD`. - Added input validation for date patterns and year ranges, displaying error messages. - New string resources for date input labels, hints, and error messages added to `strings.xml`. - **Calendar UI and UX Enhancements:** - Renamed `PersianDatePickerCalender` to `PersianDatePickerCalendar`. - Improved animations for month and day transitions. - Updated `MonthGrid`: - Displays weekday headers (`شنبه`, `یکشنبه`, etc.). - `PersianDatePickerColors` now includes `weekdaysColor`. - Better visual distinction for selected day, today's date, and current year in the year picker. - Improved `YearPicker`: - The dropdown now scrolls to the currently selected year when expanded. - Enhanced visual styling for selected and current years. - Added content descriptions for better accessibility in `PersianCalender.kt` for navigation buttons, day items, and year/month selection. - **Color Customization:** - Added `weekdaysColor` to `PersianDatePickerColors` and `PersianDatePickerDefaults` to allow customization of weekday label colors. - **Code Structure and Refinements:** - Constants like `DAYS_IN_WEEK`, `YEARS_IN_ROW`, `MAX_CALENDAR_ROWS` are now defined in `PersianCalender.kt`. - Improved use of `remember` for better performance and to avoid unnecessary recompositions. - General code cleanup and styling improvements. - **Dependency and Version Updates:** - Library version updated to `0.0.12-beta1` in `library/build.gradle.kts`. These changes aim to provide a more flexible and user-friendly date picking experience, along with better adherence to accessibility standards.
1 parent cd09225 commit 49c3d85

File tree

7 files changed

+402
-152
lines changed

7 files changed

+402
-152
lines changed

library/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ plugins {
1616
}
1717

1818
group = "io.github.faridsolgi"
19-
version = "0.0.10"
19+
version = "0.0.12-beta1"
2020

2121
kotlin {
2222
jvm()

library/src/commonMain/composeResources/values/strings.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
<string name="dateInputTitle">انتخاب تاریخ</string>
55
<string name="DatePickerHeadline">انتخاب تاریخ</string>
66
<string name="DateInputHeadline">وارد کردن تاریخ</string>
7+
<string name="date">تاریخ</string>
8+
<string name="dateHint">سال/ماه/روز</string>
79
<string name="datePickerSwitchToInputMode">Switch to input mode</string>
810
<string name="datePickerSwitchToPickerMode">Switch to picker mode</string>
11+
<string name="error_pattern_not_valid">
12+
تاریخ باید به صورت سال ۴ رقمی، سپس ماه ۲ رقمی و سپس روز ۲ رقمی وارد شود.
13+
مثال: ۱۴۰۴/۱۲/۳۰
14+
</string>
15+
<string name="error_year_not_valid_range">سال باید بین %1$d تا %2$d باشد.</string>
916
</resources>

library/src/commonMain/kotlin/io/github/faridsolgi/domain/model/PersianDatePickerColors.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ package io.github.faridsolgi.domain.model
33
import androidx.compose.runtime.Immutable
44
import androidx.compose.ui.graphics.Color
55
import androidx.compose.ui.graphics.takeOrElse
6+
import io.github.faridsolgi.persiandatetime.domain.PersianWeekday
67

78
@Immutable
89
class PersianDatePickerColors(
910
val containerColor: Color,
1011
val selectedDayColor: Color,
1112
val onSelectedDayColor: Color,
1213
val notSelectedDayColor: Color,
14+
val weekdaysColor : Color,
1315
val todayColor: Color,
1416
val titleColor: Color,
1517
val headerColor: Color,
@@ -26,9 +28,10 @@ class PersianDatePickerColors(
2628
selectedDayColor: Color = this.selectedDayColor,
2729
onSelectedDayColor: Color = this.onSelectedDayColor,
2830
notSelectedDayColor: Color = this.notSelectedDayColor,
31+
weekdaysColor: Color = this.weekdaysColor
2932
) = PersianDatePickerColors(
3033
containerColor = containerColor.takeOrElse { this.containerColor },
31-
34+
weekdaysColor = weekdaysColor.takeOrElse { this.weekdaysColor } ,
3235
titleColor = titleColor.takeOrElse { this.titleColor },
3336
headerColor = headerColor.takeOrElse { this.headerColor },
3437
confirmButtonColor = confirmButtonColor.takeOrElse { this.confirmButtonColor },
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package io.github.faridsolgi.util
2+
3+
import androidx.compose.material3.ExperimentalMaterial3Api
4+
import androidx.compose.ui.text.AnnotatedString
5+
import androidx.compose.ui.text.input.OffsetMapping
6+
import androidx.compose.ui.text.input.TransformedText
7+
import androidx.compose.ui.text.input.VisualTransformation
8+
9+
@OptIn(ExperimentalMaterial3Api::class)
10+
internal class DateVisualTransformation() : VisualTransformation {
11+
12+
private val firstDelimiterOffset = 4 // after year
13+
private val secondDelimiterOffset = 6 // after month
14+
private val maxLength = 8 // YYYYMMDD
15+
16+
private val offsetTranslator = object : OffsetMapping {
17+
override fun originalToTransformed(offset: Int): Int {
18+
return when {
19+
offset <= firstDelimiterOffset -> offset
20+
offset <= secondDelimiterOffset -> offset + 1
21+
offset <= maxLength -> offset + 2
22+
else -> maxLength + 2 // max transformed = 10 (YYYY/MM/DD)
23+
}
24+
}
25+
26+
override fun transformedToOriginal(offset: Int): Int {
27+
return when {
28+
offset <= firstDelimiterOffset -> offset
29+
offset <= secondDelimiterOffset -> offset - 1
30+
offset <= maxLength + 1 -> offset - 2
31+
else -> maxLength
32+
}
33+
}
34+
}
35+
36+
override fun filter(text: AnnotatedString): TransformedText {
37+
val trimmed = text.text.take(maxLength) // only digits
38+
val transformed = buildString {
39+
trimmed.forEachIndexed { index, c ->
40+
append(c)
41+
if (index + 1 == firstDelimiterOffset || index + 1 == secondDelimiterOffset) {
42+
append("/")
43+
}
44+
}
45+
}
46+
return TransformedText(AnnotatedString(transformed), offsetTranslator)
47+
}
48+
}
49+

library/src/commonMain/kotlin/io/github/faridsolgi/view/PersianDatePicker.kt

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@ package io.github.faridsolgi.view
22

33

44
import androidx.compose.animation.AnimatedContent
5-
import androidx.compose.animation.AnimatedVisibility
65
import androidx.compose.animation.core.tween
76
import androidx.compose.animation.fadeIn
87
import androidx.compose.animation.fadeOut
98
import androidx.compose.animation.slideInHorizontally
10-
import androidx.compose.animation.slideInVertically
119
import androidx.compose.animation.slideOutHorizontally
12-
import androidx.compose.animation.slideOutVertically
1310
import androidx.compose.animation.togetherWith
1411
import androidx.compose.foundation.background
1512
import androidx.compose.foundation.layout.Arrangement
@@ -21,27 +18,53 @@ import androidx.compose.foundation.layout.fillMaxWidth
2118
import androidx.compose.foundation.layout.padding
2219
import androidx.compose.foundation.layout.sizeIn
2320
import androidx.compose.material3.DatePicker
21+
import androidx.compose.material3.DatePickerDefaults
22+
import androidx.compose.material3.DatePickerFormatter
2423
import androidx.compose.material3.ExperimentalMaterial3Api
2524
import androidx.compose.material3.HorizontalDivider
25+
import androidx.compose.material3.LocalTextStyle
2626
import androidx.compose.material3.MaterialTheme
27+
import androidx.compose.material3.OutlinedTextField
2728
import androidx.compose.material3.Text
2829
import androidx.compose.material3.TextButton
30+
import androidx.compose.material3.rememberDatePickerState
2931
import androidx.compose.runtime.Composable
3032
import androidx.compose.runtime.CompositionLocalProvider
33+
import androidx.compose.runtime.LaunchedEffect
34+
import androidx.compose.runtime.getValue
35+
import androidx.compose.runtime.mutableStateOf
36+
import androidx.compose.runtime.remember
37+
import androidx.compose.runtime.setValue
3138
import androidx.compose.ui.Alignment
3239
import androidx.compose.ui.Modifier
3340
import androidx.compose.ui.platform.LocalLayoutDirection
41+
import androidx.compose.ui.text.AnnotatedString
42+
import androidx.compose.ui.text.input.OffsetMapping
43+
import androidx.compose.ui.text.input.TransformedText
44+
import androidx.compose.ui.text.input.VisualTransformation
45+
import androidx.compose.ui.text.style.TextAlign
3446
import androidx.compose.ui.unit.LayoutDirection
3547
import androidx.compose.ui.unit.dp
3648
import io.github.faridsolgi.domain.model.DisplayMode
3749
import io.github.faridsolgi.domain.model.PersianDatePickerColors
3850
import io.github.faridsolgi.domain.model.PersianDatePickerTokens
51+
import io.github.faridsolgi.library.generated.resources.Res
52+
import io.github.faridsolgi.library.generated.resources.date
53+
import io.github.faridsolgi.library.generated.resources.dateHint
54+
import io.github.faridsolgi.library.generated.resources.error_pattern_not_valid
55+
import io.github.faridsolgi.library.generated.resources.error_year_not_valid_range
56+
import io.github.faridsolgi.persiandatetime.converter.format
3957
import io.github.faridsolgi.persiandatetime.converter.toDateString
58+
import io.github.faridsolgi.persiandatetime.domain.PersianDateTime
59+
import io.github.faridsolgi.util.DateVisualTransformation
4060
import io.github.faridsolgi.view.internal.DisplayModeToggleButton
41-
import io.github.faridsolgi.view.internal.PersianDatePickerCalender
61+
import io.github.faridsolgi.view.internal.PersianDatePickerCalendar
4262
import io.github.faridsolgi.view.internal.ProvideContentColorTextStyle
63+
import kotlinx.datetime.toLocalDateTime
64+
import org.jetbrains.compose.resources.stringResource
4365
import org.jetbrains.compose.ui.tooling.preview.Preview
4466

67+
@OptIn(ExperimentalMaterial3Api::class)
4568
@Composable
4669
fun PersianDatePicker(
4770
state: PersianDatePickerState,
@@ -63,7 +86,6 @@ fun PersianDatePicker(
6386
showModeToggle: Boolean = true,
6487
colors: PersianDatePickerColors = PersianDatePickerDefaults.colors(),
6588
) {
66-
//DatePicker()
6789
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
6890
Column(modifier) {
6991
PersianDatePickerHeadLine(
@@ -93,25 +115,88 @@ fun SwitchablePersianDatePickerContents(
93115
AnimatedContent(
94116
targetState = state.displayMode ,
95117
transitionSpec = {
96-
slideInVertically(animationSpec = tween(10000)) { height -> height } + fadeIn() togetherWith
97-
slideOutVertically(animationSpec = tween(1000)) { height -> -height } + fadeOut()
118+
slideInHorizontally(animationSpec = tween(500)) { height -> height } + fadeIn() togetherWith slideOutHorizontally(
119+
animationSpec = tween(500)
120+
) { height -> -height } + fadeOut()
98121
},
99122
label = "display mode transition"
100123
) { displayMode ->
101124
Column(modifier) {
102125
when (displayMode) {
103126
DisplayMode.Companion.Picker -> {
104-
PersianDatePickerCalender(state, colors)
127+
PersianDatePickerCalendar(state, colors)
105128
}
106129

107130
DisplayMode.Companion.Input -> {
108-
Text("test input")
131+
PersianDateEnterSection(state, colors)
109132
}
110133
}
111134
}
112135
}
113136
}
114137

138+
@Composable
139+
internal fun PersianDateEnterSection(
140+
state: PersianDatePickerState,
141+
colors: PersianDatePickerColors,
142+
) {
143+
var enteredDate by remember { mutableStateOf(state.selectedDate?.format { year();month();day(); }?:"") }
144+
var isError by remember { mutableStateOf(false) }
145+
var errorText by remember { mutableStateOf("") }
146+
val errorPatternNotValid = stringResource(Res.string.error_pattern_not_valid)
147+
val errorYearNotValidRange = stringResource(Res.string.error_year_not_valid_range,state.yearRange.first,state.yearRange.last)
148+
LaunchedEffect(enteredDate) {
149+
if (enteredDate.length == 8) {
150+
val year = enteredDate.substring(0, 4).toIntOrNull()
151+
val month = enteredDate.substring(4, 6).toIntOrNull()
152+
val day = enteredDate.substring(6, 8).toIntOrNull()
153+
if (year == null || year !in 1300..1500) {
154+
isError = true
155+
errorText = errorYearNotValidRange
156+
state.selectedDate =null
157+
return@LaunchedEffect
158+
}
159+
try {
160+
val validDate = PersianDateTime(year=year,month=month!!,day=day!!)
161+
state.selectedDate = validDate
162+
}catch (e: IllegalArgumentException){
163+
isError = true
164+
errorText = e.message.toString()
165+
state.selectedDate =null
166+
}catch (e: Exception){
167+
isError = true
168+
errorText = errorPatternNotValid
169+
state.selectedDate =null
170+
}
171+
} else {
172+
isError = false
173+
errorText = ""
174+
state.selectedDate =null
175+
}
176+
}
177+
178+
OutlinedTextField(
179+
modifier = Modifier.fillMaxWidth()
180+
.padding(horizontal = 24.dp)
181+
.padding(top = 16.dp),
182+
value = enteredDate,
183+
onValueChange = { newValue ->
184+
// Keep only digits
185+
val digits = newValue.filter { it.isDigit() }
186+
enteredDate = digits.take(10)
187+
},
188+
isError = isError,
189+
supportingText = {
190+
Text(errorText,
191+
style = LocalTextStyle.current.copy(textAlign = TextAlign.Start)
192+
)
193+
},
194+
label = { Text(stringResource(Res.string.date)) },
195+
placeholder = { Text(stringResource(Res.string.dateHint)) },
196+
singleLine = true,
197+
visualTransformation = DateVisualTransformation(),
198+
)
199+
}
115200

116201
@OptIn(ExperimentalMaterial3Api::class)
117202
@Composable

library/src/commonMain/kotlin/io/github/faridsolgi/view/PersianDatePickerDefaults.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ object PersianDatePickerDefaults {
4444
onSelectedDayColor: Color = Color.Unspecified,
4545
notSelectedDayColor: Color = Color.Unspecified,
4646
todayColor: Color = Color.Unspecified,
47+
weekdaysColor: Color =Color.Unspecified
4748

4849
): PersianDatePickerColors = MaterialTheme.colorScheme.DefaultPersianDatePickerColors.copy(
4950
containerColor = containerColor,
@@ -53,7 +54,8 @@ object PersianDatePickerDefaults {
5354
selectedDayColor = selectedDayColor,
5455
onSelectedDayColor = onSelectedDayColor,
5556
notSelectedDayColor = notSelectedDayColor,
56-
todayColor = todayColor
57+
todayColor = todayColor,
58+
weekdaysColor = weekdaysColor
5759
)
5860

5961

@@ -69,7 +71,8 @@ object PersianDatePickerDefaults {
6971
selectedDayColor = this.primary,
7072
onSelectedDayColor = this.onPrimary,
7173
notSelectedDayColor = this.onSurface,
72-
todayColor = this.primary
74+
todayColor = this.primary,
75+
weekdaysColor = this.onSurface
7376
)
7477
}
7578

0 commit comments

Comments
 (0)