Skip to content

Commit 82ffa5a

Browse files
LynxLynxxdtscalacdamian-molinski
authored
feat(cat-voices): Date & Time input widget (#1224)
* feat: ui widget for date picker commponent * feat: custom controller for date picker widget * feat: adding validation to textfields * feat: enhance date picker with improved validation and error handling messages * feat: improve overlay management in date picker and enhance scroll controller handling in text fields * fix: overlay switch between date and time * chore: remove cached gitignore files * fix: update OK button text in VoicesCalendarDatePicker for localization consistency * feat: refactor date and time pickers to use DateTime for better consistency and state management across the application * feat: implement new date and time picker modules with validation and controllers for better UI interaction * fix: static-analytics * refactor: date time text field (#1293) * refactor: date time text field * fix: doc reference * feat: add date/time input formatting to date and time fields * fix: late final in voices_date_field.dart * fix: late final in voices_time_field.dart * fix: formatting --------- Co-authored-by: Dominik Toton <[email protected]> Co-authored-by: Damian Moliński <[email protected]>
1 parent 67930c6 commit 82ffa5a

File tree

16 files changed

+1169
-143
lines changed

16 files changed

+1169
-143
lines changed

.config/dictionaries/project.dic

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ auditability
2222
Autolayout
2323
autorecalculates
2424
autoresizing
25+
autovalidate
2526
backendpython
2627
bech
2728
bimap

catalyst_voices/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
### Dart ###
22
# See https://www.dartlang.org/guides/libraries/private-files
3+
devtools_options.yaml
34

45

56
# Generated files from code generation tools
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import 'package:flutter/material.dart';
2+
3+
extension TimeOfDayExt on TimeOfDay {
4+
String get formatted {
5+
final hour = this.hour.toString().padLeft(2, '0');
6+
final minute = this.minute.toString().padLeft(2, '0');
7+
8+
return '$hour:$minute';
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart';
2+
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
3+
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
4+
import 'package:flutter/material.dart';
5+
6+
class VoicesCalendarDatePicker extends StatefulWidget {
7+
final ValueChanged<DateTime> onDateSelected;
8+
final VoidCallback cancelEvent;
9+
final DateTime initialDate;
10+
final DateTime firstDate;
11+
final DateTime lastDate;
12+
13+
factory VoicesCalendarDatePicker({
14+
Key? key,
15+
required ValueChanged<DateTime> onDateSelected,
16+
required VoidCallback cancelEvent,
17+
DateTime? initialDate,
18+
DateTime? firstDate,
19+
DateTime? lastDate,
20+
}) {
21+
final now = DateTime.now();
22+
return VoicesCalendarDatePicker._(
23+
key: key,
24+
onDateSelected: onDateSelected,
25+
initialDate: initialDate ?? now,
26+
firstDate: firstDate ?? now,
27+
lastDate: lastDate ?? DateTime(now.year + 1, now.month, now.day),
28+
cancelEvent: cancelEvent,
29+
);
30+
}
31+
32+
const VoicesCalendarDatePicker._({
33+
super.key,
34+
required this.onDateSelected,
35+
required this.initialDate,
36+
required this.firstDate,
37+
required this.lastDate,
38+
required this.cancelEvent,
39+
});
40+
41+
@override
42+
State<VoicesCalendarDatePicker> createState() =>
43+
_VoicesCalendarDatePickerState();
44+
}
45+
46+
class _VoicesCalendarDatePickerState extends State<VoicesCalendarDatePicker> {
47+
DateTime selectedDate = DateTime.now();
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
return SizedBox(
52+
width: 450,
53+
child: Material(
54+
clipBehavior: Clip.hardEdge,
55+
color: Colors.transparent,
56+
child: DecoratedBox(
57+
decoration: BoxDecoration(
58+
color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey,
59+
borderRadius: BorderRadius.circular(20),
60+
),
61+
child: Column(
62+
children: [
63+
CalendarDatePicker(
64+
initialDate: widget.initialDate,
65+
firstDate: widget.firstDate,
66+
lastDate: widget.lastDate,
67+
onDateChanged: (val) {
68+
selectedDate = val;
69+
},
70+
),
71+
Padding(
72+
padding: const EdgeInsets.all(8),
73+
child: Row(
74+
mainAxisAlignment: MainAxisAlignment.end,
75+
children: [
76+
VoicesTextButton(
77+
onTap: widget.cancelEvent,
78+
child: Text(context.l10n.cancelButtonText),
79+
),
80+
VoicesTextButton(
81+
onTap: () => widget.onDateSelected(selectedDate),
82+
child: Text(context.l10n.ok.toUpperCase()),
83+
),
84+
],
85+
),
86+
),
87+
],
88+
),
89+
),
90+
),
91+
);
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import 'package:catalyst_voices/common/ext/time_of_day_ext.dart';
2+
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
3+
import 'package:flutter/material.dart';
4+
5+
class VoicesTimePicker extends StatefulWidget {
6+
final ValueChanged<TimeOfDay> onTap;
7+
final TimeOfDay? selectedTime;
8+
final String? timeZone;
9+
10+
const VoicesTimePicker({
11+
super.key,
12+
required this.onTap,
13+
this.selectedTime,
14+
this.timeZone,
15+
});
16+
17+
@override
18+
State<VoicesTimePicker> createState() => _VoicesTimePickerState();
19+
}
20+
21+
class _VoicesTimePickerState extends State<VoicesTimePicker> {
22+
late final ScrollController _scrollController;
23+
late final List<TimeOfDay> _timeList;
24+
25+
final double itemExtent = 40;
26+
27+
@override
28+
void initState() {
29+
super.initState();
30+
31+
_timeList = _generateTimeList();
32+
_scrollController = ScrollController();
33+
34+
final initialSelection = widget.selectedTime;
35+
if (initialSelection != null) {
36+
WidgetsBinding.instance.addPostFrameCallback((_) {
37+
_scrollToTime(initialSelection);
38+
});
39+
}
40+
}
41+
42+
@override
43+
void dispose() {
44+
_scrollController.dispose();
45+
super.dispose();
46+
}
47+
48+
@override
49+
Widget build(BuildContext context) {
50+
return Material(
51+
type: MaterialType.transparency,
52+
clipBehavior: Clip.hardEdge,
53+
child: Container(
54+
height: 350,
55+
width: 150,
56+
decoration: BoxDecoration(
57+
color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey,
58+
borderRadius: BorderRadius.circular(20),
59+
),
60+
child: ListView.builder(
61+
controller: _scrollController,
62+
itemExtent: itemExtent,
63+
itemCount: _timeList.length,
64+
itemBuilder: (context, index) {
65+
final timeOfDay = _timeList[index];
66+
67+
return _TimeText(
68+
key: ValueKey(timeOfDay.formatted),
69+
value: timeOfDay,
70+
onTap: widget.onTap,
71+
isSelected: timeOfDay == widget.selectedTime,
72+
timeZone: widget.timeZone,
73+
);
74+
},
75+
),
76+
),
77+
);
78+
}
79+
80+
void _scrollToTime(TimeOfDay value) {
81+
final index = _timeList.indexWhere((e) => e == widget.selectedTime);
82+
83+
if (index != -1) {
84+
_scrollController.jumpTo(index * itemExtent);
85+
}
86+
}
87+
88+
List<TimeOfDay> _generateTimeList() {
89+
return [
90+
for (var hour = 0; hour < 24; hour++)
91+
for (final minute in [0, 30]) TimeOfDay(hour: hour, minute: minute),
92+
];
93+
}
94+
}
95+
96+
class _TimeText extends StatelessWidget {
97+
final ValueChanged<TimeOfDay> onTap;
98+
final TimeOfDay value;
99+
final bool isSelected;
100+
final String? timeZone;
101+
102+
const _TimeText({
103+
super.key,
104+
required this.value,
105+
required this.onTap,
106+
this.isSelected = false,
107+
this.timeZone,
108+
});
109+
110+
@override
111+
Widget build(BuildContext context) {
112+
final timeZone = this.timeZone;
113+
114+
return Material(
115+
clipBehavior: Clip.hardEdge,
116+
type: MaterialType.transparency,
117+
child: InkWell(
118+
onTap: () => onTap(value),
119+
child: ColoredBox(
120+
color: !isSelected
121+
? Colors.transparent
122+
: Theme.of(context).colors.onSurfaceNeutral08!,
123+
child: Padding(
124+
key: key,
125+
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
126+
child: Row(
127+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
128+
children: [
129+
Text(
130+
value.formatted,
131+
style: Theme.of(context).textTheme.bodyLarge,
132+
),
133+
if (isSelected && timeZone != null) Text(timeZone),
134+
],
135+
),
136+
),
137+
),
138+
),
139+
);
140+
}
141+
}

0 commit comments

Comments
 (0)