Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cat-voices): Date & Time input widget #1224

Merged
merged 20 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
beef287
feat: ui widget for date picker commponent
LynxLynxx Nov 15, 2024
9af1585
feat: custom controller for date picker widget
LynxLynxx Nov 15, 2024
083439e
feat: adding validation to textfields
LynxLynxx Nov 15, 2024
8c71ef1
feat: enhance date picker with improved validation and error handling…
LynxLynxx Nov 18, 2024
44db8a9
feat: improve overlay management in date picker and enhance scroll co…
LynxLynxx Nov 18, 2024
489a7ea
fix: overlay switch between date and time
LynxLynxx Nov 18, 2024
6e76dae
Merge branch 'main' into feature/1181-date-time-widget
LynxLynxx Nov 18, 2024
82bf505
chore: remove cached gitignore files
LynxLynxx Nov 18, 2024
8667ccf
fix: update OK button text in VoicesCalendarDatePicker for localizati…
LynxLynxx Nov 18, 2024
84bc0ee
feat: refactor date and time pickers to use DateTime for better consi…
LynxLynxx Nov 19, 2024
43c2f50
Merge branch 'mve3' into feature/1181-date-time-widget
LynxLynxx Nov 26, 2024
d9afad0
feat: implement new date and time picker modules with validation and …
LynxLynxx Nov 26, 2024
b09e9f8
fix: static-analytics
LynxLynxx Nov 27, 2024
594fca9
Merge branch 'mve3' into feature/1181-date-time-widget
dtscalac Nov 27, 2024
7fcb3af
refactor: date time text field (#1293)
damian-molinski Nov 29, 2024
20bba99
feat: add date/time input formatting to date and time fields
LynxLynxx Nov 29, 2024
3a0f77d
Merge branch 'mve3' into feature/1181-date-time-widget
damian-molinski Nov 29, 2024
d4c0d30
fix: late final in voices_date_field.dart
LynxLynxx Nov 29, 2024
fa68604
fix: late final in voices_time_field.dart
LynxLynxx Nov 29, 2024
447da0c
fix: formatting
LynxLynxx Nov 29, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .config/dictionaries/project.dic
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ auditability
Autolayout
autorecalculates
autoresizing
autovalidate
backendpython
bech
bimap
Expand Down
1 change: 1 addition & 0 deletions catalyst_voices/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
### Dart ###
# See https://www.dartlang.org/guides/libraries/private-files
devtools_options.yaml


# Generated files from code generation tools
Expand Down
10 changes: 10 additions & 0 deletions catalyst_voices/apps/voices/lib/common/ext/time_of_day_ext.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:flutter/material.dart';

extension TimeOfDayExt on TimeOfDay {
String get formatted {
final hour = this.hour.toString().padLeft(2, '0');
final minute = this.minute.toString().padLeft(2, '0');

return '$hour:$minute';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:catalyst_voices_localization/catalyst_voices_localization.dart';
import 'package:flutter/material.dart';

class VoicesCalendarDatePicker extends StatefulWidget {
final ValueChanged<DateTime> onDateSelected;
final VoidCallback cancelEvent;
final DateTime initialDate;
final DateTime firstDate;
final DateTime lastDate;

factory VoicesCalendarDatePicker({
Key? key,
required ValueChanged<DateTime> onDateSelected,
required VoidCallback cancelEvent,
DateTime? initialDate,
DateTime? firstDate,
DateTime? lastDate,
}) {
final now = DateTime.now();
return VoicesCalendarDatePicker._(
key: key,
onDateSelected: onDateSelected,
initialDate: initialDate ?? now,
firstDate: firstDate ?? now,
lastDate: lastDate ?? DateTime(now.year + 1, now.month, now.day),
cancelEvent: cancelEvent,
);
}

const VoicesCalendarDatePicker._({
super.key,
required this.onDateSelected,
required this.initialDate,
required this.firstDate,
required this.lastDate,
required this.cancelEvent,
});

@override
State<VoicesCalendarDatePicker> createState() =>
_VoicesCalendarDatePickerState();
}

class _VoicesCalendarDatePickerState extends State<VoicesCalendarDatePicker> {
DateTime selectedDate = DateTime.now();

@override
Widget build(BuildContext context) {
return SizedBox(
width: 450,
child: Material(
clipBehavior: Clip.hardEdge,
color: Colors.transparent,
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey,
borderRadius: BorderRadius.circular(20),
),
child: Column(
children: [
CalendarDatePicker(
initialDate: widget.initialDate,
firstDate: widget.firstDate,
lastDate: widget.lastDate,
onDateChanged: (val) {
selectedDate = val;
},
),
Padding(
padding: const EdgeInsets.all(8),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
VoicesTextButton(
onTap: widget.cancelEvent,
child: Text(context.l10n.cancelButtonText),
),
VoicesTextButton(
onTap: () => widget.onDateSelected(selectedDate),
child: Text(context.l10n.ok.toUpperCase()),
),
],
),
),
],
),
),
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import 'package:catalyst_voices/common/ext/time_of_day_ext.dart';
import 'package:catalyst_voices_brands/catalyst_voices_brands.dart';
import 'package:flutter/material.dart';

class VoicesTimePicker extends StatefulWidget {
final ValueChanged<TimeOfDay> onTap;
final TimeOfDay? selectedTime;
final String? timeZone;

const VoicesTimePicker({
super.key,
required this.onTap,
this.selectedTime,
this.timeZone,
});

@override
State<VoicesTimePicker> createState() => _VoicesTimePickerState();
}

class _VoicesTimePickerState extends State<VoicesTimePicker> {
late final ScrollController _scrollController;
late final List<TimeOfDay> _timeList;

final double itemExtent = 40;

@override
void initState() {
super.initState();

_timeList = _generateTimeList();
_scrollController = ScrollController();

final initialSelection = widget.selectedTime;
if (initialSelection != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_scrollToTime(initialSelection);
});
}
}

@override
void dispose() {
_scrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
clipBehavior: Clip.hardEdge,
child: Container(
height: 350,
width: 150,
decoration: BoxDecoration(
color: Theme.of(context).colors.elevationsOnSurfaceNeutralLv1Grey,
borderRadius: BorderRadius.circular(20),
),
child: ListView.builder(
controller: _scrollController,
itemExtent: itemExtent,
itemCount: _timeList.length,
itemBuilder: (context, index) {
final timeOfDay = _timeList[index];

return _TimeText(
key: ValueKey(timeOfDay.formatted),
value: timeOfDay,
onTap: widget.onTap,
isSelected: timeOfDay == widget.selectedTime,
timeZone: widget.timeZone,
);
},
),
),
);
}

void _scrollToTime(TimeOfDay value) {
final index = _timeList.indexWhere((e) => e == widget.selectedTime);

if (index != -1) {
_scrollController.jumpTo(index * itemExtent);
}
}

List<TimeOfDay> _generateTimeList() {
return [
for (var hour = 0; hour < 24; hour++)
for (final minute in [0, 30]) TimeOfDay(hour: hour, minute: minute),
];
}
}

class _TimeText extends StatelessWidget {
final ValueChanged<TimeOfDay> onTap;
final TimeOfDay value;
final bool isSelected;
final String? timeZone;

const _TimeText({
super.key,
required this.value,
required this.onTap,
this.isSelected = false,
this.timeZone,
});

@override
Widget build(BuildContext context) {
final timeZone = this.timeZone;

return Material(
clipBehavior: Clip.hardEdge,
type: MaterialType.transparency,
child: InkWell(
onTap: () => onTap(value),
child: ColoredBox(
color: !isSelected
? Colors.transparent
: Theme.of(context).colors.onSurfaceNeutral08!,
child: Padding(
key: key,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
value.formatted,
style: Theme.of(context).textTheme.bodyLarge,
),
if (isSelected && timeZone != null) Text(timeZone),
],
),
),
),
),
);
}
}
Loading
Loading