diff --git a/docs/pages/docs/calendar.mdx b/docs/pages/docs/calendar.mdx new file mode 100644 index 000000000..e61f480fb --- /dev/null +++ b/docs/pages/docs/calendar.mdx @@ -0,0 +1,91 @@ +import { Tabs } from 'nextra/components'; +import { Widget } from "../../components/widget"; + +# Calendar +A date field component that allows users to enter and edit date. + +The calendar pages are designed to be navigable through swipe gestures on mobile platforms, allowing left and right swipes +to transition between pages. + + + + + + + ```dart + FCalendar( + controller: FCalendarSingleRangeController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ); + ``` + + + +## Usage + +### `FCalendar(...)` + +```dart +FCalendar( + controller: FCalendarSingleRangeController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + today: DateTime.utc(2024, 7, 14), + initalType = FCalendarPickerType.yearMonth, + initialDate = DateTime.utc(2024, 9, 12), + enabled: (date) => allowed.contains(date), + onMonthChange: (date) => print(date), + onPress: (date) => print(date), + onLongPress: (date) => print(date), +); +``` +## Examples + +### Single Date + + + + + + ```dart + FCalendar( + controller: FCalendarSingleValueController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ); + ``` + + + +### Multiple Dates + + + + + + ```dart + FCalendar( + controller: FCalendarMultiValueController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ); + ``` + + + +### Single Range + + + + + + ```dart + FCalendar( + controller: FCalendarSingleRangeController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ) + ``` + + diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index ea9447369..66d455791 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -1,5 +1,8 @@ ## Next +### Additions +* Add `FCalendar` + ### Enhancements * **Breaking** Change `FSwitch` to be usable in `Form`s. * **Breaking** Rename `FThemeData.checkBoxStyle` to `FThemeData.checkboxStyle` for consistency. diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index 7bddef031..d22d19137 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; +import 'package:forui_example/example.dart'; void main() { @@ -29,27 +30,6 @@ class Application extends StatelessWidget { content: child ?? const SizedBox(), ), ), - home: Column( - children: [ - const Testing(), - // FHeaderAction( - // icon: FAssets.icons.plus, - // onPress: () => showDatePicker(context: context, firstDate: DateTime(2024, 7, 1), lastDate: DateTime(2024, 7, 31), initialDate: DateTime(2024, 7, 8)), - // ), - ], - ), - ); -} - -class Testing extends StatelessWidget { - static final _selected = {DateTime(2024, 7, 16), DateTime(2024, 7, 17), DateTime(2024, 7, 18), DateTime(2024, 7, 29)}; - - const Testing({super.key}); - - @override - Widget build(BuildContext context) => FCalendar( - start: DateTime(1900, 1, 8), - end: DateTime(2024, 7, 10), - controller: FCalendarMultiValueController(), + home: const Example(), ); } diff --git a/forui/lib/src/widgets/calendar/calendar.dart b/forui/lib/src/widgets/calendar/calendar.dart index c3d36a645..e2a03f38f 100644 --- a/forui/lib/src/widgets/calendar/calendar.dart +++ b/forui/lib/src/widgets/calendar/calendar.dart @@ -16,6 +16,9 @@ export 'year_month_picker.dart' show FCalendarYearMonthPickerStyle; /// A calendar. /// +/// The calendar pages are designed to be navigable through swipe gestures on mobile Android, iOS & iPadOS, allowing +/// left and right swipes to transition between pages. +/// /// See: /// * https://forui.dev/docs/calendar for working examples. /// * [FCalendarDayStyle] for customizing a card's appearance. @@ -104,9 +107,7 @@ class FCalendar extends StatelessWidget { ValueListenableBuilder( valueListenable: _type, builder: (context, value, child) => switch (value) { - FCalendarPickerType.day => ValueListenableBuilder( - valueListenable: controller, - builder: (context, _, __) => PagedDayPicker( + FCalendarPickerType.day => PagedDayPicker( style: style, start: start.toLocalDate(), end: end.toLocalDate(), @@ -125,12 +126,12 @@ class FCalendar extends StatelessWidget { }, onLongPress: (date) => onLongPress?.call(date.toNative()), ), - ), FCalendarPickerType.yearMonth => YearMonthPicker( style: style, start: start.toLocalDate(), end: end.toLocalDate(), today: today.toLocalDate(), + initial: _month.value, onChange: (date) { _month.value = date; _type.value = FCalendarPickerType.day; diff --git a/forui/lib/src/widgets/calendar/calendar_controller.dart b/forui/lib/src/widgets/calendar/calendar_controller.dart index a92fab906..35e3dc762 100644 --- a/forui/lib/src/widgets/calendar/calendar_controller.dart +++ b/forui/lib/src/widgets/calendar/calendar_controller.dart @@ -3,6 +3,11 @@ import 'package:forui/forui.dart'; import 'package:sugar/sugar.dart'; /// A controller that controls date selection in a calendar. +/// +/// This class should be extended to customize date selection. By default, the following controllers are provided: +/// * [FCalendarSingleValueController] for selecting a single date. +/// * [FCalendarMultiValueController] for selecting multiple date. +/// * [FCalendarSingleRangeController] for selecting a single range. abstract class FCalendarController extends ValueNotifier { /// Creates a [FCalendarController] with the given initial [value]. FCalendarController(super._value); @@ -30,13 +35,7 @@ final class FCalendarSingleValueController extends FCalendarController value?.toLocalDate() == date.toLocalDate(); @override - void onPress(DateTime date) { - if (value?.toLocalDate() == date.toLocalDate()) { - value = null; - } else { - value = date; - } - } + void onPress(DateTime date) => value = value?.toLocalDate() == date.toLocalDate() ? null : date; } /// A date selection controller that allows multiple dates to be selected. @@ -47,14 +46,15 @@ final class FCalendarMultiValueController extends FCalendarController d.isUtc), 'dates must be in UTC timezone'); + FCalendarMultiValueController([super.value = const {}]) + : assert(value.every((d) => d.isUtc), 'dates must be in UTC timezone'); @override bool contains(DateTime date) => value.contains(date); @override void onPress(DateTime date) { - final copy = { ...value }; + final copy = {...value}; value = copy..toggle(date); } } @@ -67,9 +67,15 @@ final class FCalendarSingleRangeController extends FCalendarController<(DateTime /// Creates a [FCalendarSingleRangeController] with the given initial [value]. /// /// ## Contract: - /// Throws an [AssertionError] if the given [value] is not in UTC timezone. + /// Throws an [AssertionError] if: + /// * the given dates in [value] is not in UTC timezone. + /// * the end date is less than start date. FCalendarSingleRangeController([super.value]) - : assert(value == null || (value.$1.isUtc && value.$2.isUtc), 'value must be in UTC timezone'); + : assert(value == null || (value.$1.isUtc && value.$2.isUtc), 'value must be in UTC timezone'), + assert( + value == null || (value.$1.isBefore(value.$2) || value.$1.isAtSameMomentAs(value.$2)), + 'end date must be greater than or equal to start date', + ); @override bool contains(DateTime date) { @@ -98,7 +104,7 @@ final class FCalendarSingleRangeController extends FCalendarController<(DateTime case (final first, final last) when pressed < first: value = (pressed.toNative(), last.toNative()); - case (final first, _): + case (final first, _): value = (first.toNative(), pressed.toNative()); } } diff --git a/forui/lib/src/widgets/calendar/month/month_picker.dart b/forui/lib/src/widgets/calendar/month/month_picker.dart index cad228eb8..6c17bc46b 100644 --- a/forui/lib/src/widgets/calendar/month/month_picker.dart +++ b/forui/lib/src/widgets/calendar/month/month_picker.dart @@ -6,7 +6,8 @@ import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; import 'package:sugar/sugar.dart'; -final _yMMMM = DateFormat.yMMMM(); +// ignore: non_constant_identifier_names +final _MMM = DateFormat.MMM(); @internal class MonthPicker extends StatefulWidget { @@ -74,7 +75,7 @@ class _MonthPickerState extends State { focusNode: _months[i], current: widget.today.truncate(to: DateUnit.months) == month, enabled: widget.start <= month && month <= widget.end, - format: (date) => _yMMMM.format(date.toNative()), // TODO: localize + format: (date) => _MMM.format(date.toNative()), // TODO: localize onPress: widget.onPress, ), ], diff --git a/forui/lib/src/widgets/calendar/shared/header.dart b/forui/lib/src/widgets/calendar/shared/header.dart index facb8e82a..591cc3589 100644 --- a/forui/lib/src/widgets/calendar/shared/header.dart +++ b/forui/lib/src/widgets/calendar/shared/header.dart @@ -51,8 +51,9 @@ class _HeaderState extends State
with SingleTickerProviderStateMixin { @override void initState() { super.initState(); - _controller = AnimationController(vsync: this, duration: widget.style.animationDuration); widget.type.addListener(_animate); + _controller = AnimationController(vsync: this, duration: widget.style.animationDuration); + _controller.value = widget.type.value == FCalendarPickerType.day ? 0.0 : 1.0; } @override @@ -60,8 +61,8 @@ class _HeaderState extends State
with SingleTickerProviderStateMixin { height: Header.height, child: FInkWell( onPress: () => widget.type.value = switch (widget.type.value) { - FCalendarPickerType.day => FCalendarPickerType.day, - FCalendarPickerType.yearMonth => FCalendarPickerType.yearMonth, + FCalendarPickerType.day => FCalendarPickerType.yearMonth, + FCalendarPickerType.yearMonth => FCalendarPickerType.day, }, builder: (context, _, child) => child!, child: Row( @@ -98,10 +99,14 @@ class _HeaderState extends State
with SingleTickerProviderStateMixin { } void _animate() { - if (_controller.isCompleted) { - _controller.reverse(); - } else { - _controller.forward(); + // we check the picker type to prevent de-syncs + switch ((widget.type.value, _controller.isCompleted)) { + case (FCalendarPickerType.yearMonth, false): + _controller.forward(); + case (FCalendarPickerType.day, true): + _controller.reverse(); + + case _: } } } diff --git a/forui/lib/src/widgets/calendar/year_month_picker.dart b/forui/lib/src/widgets/calendar/year_month_picker.dart index e5ea884a5..9198ea65e 100644 --- a/forui/lib/src/widgets/calendar/year_month_picker.dart +++ b/forui/lib/src/widgets/calendar/year_month_picker.dart @@ -12,6 +12,7 @@ class YearMonthPicker extends StatefulWidget { final LocalDate start; final LocalDate end; final LocalDate today; + final LocalDate initial; final ValueChanged onChange; const YearMonthPicker({ @@ -19,6 +20,7 @@ class YearMonthPicker extends StatefulWidget { required this.start, required this.end, required this.today, + required this.initial, required this.onChange, super.key, }); @@ -34,6 +36,7 @@ class YearMonthPicker extends StatefulWidget { ..add(DiagnosticsProperty('start', start)) ..add(DiagnosticsProperty('end', end)) ..add(DiagnosticsProperty('today', today)) + ..add(DiagnosticsProperty('initial', initial)) ..add(DiagnosticsProperty('onChange', onChange)); } } @@ -54,7 +57,7 @@ class _YearMonthPickerState extends State { start: widget.start, end: widget.end, today: widget.today, - initial: widget.today.truncate(to: DateUnit.years), + initial: widget.initial.truncate(to: DateUnit.years), onPress: (year) => setState(() => _date = year), ); } else { diff --git a/forui/test/golden/calendar/day-picker/zinc-dark-default.png b/forui/test/golden/calendar/day-picker/zinc-dark-default.png new file mode 100644 index 000000000..0f3dfc9db Binary files /dev/null and b/forui/test/golden/calendar/day-picker/zinc-dark-default.png differ diff --git a/forui/test/golden/calendar/day-picker/zinc-dark-max-rows.png b/forui/test/golden/calendar/day-picker/zinc-dark-max-rows.png new file mode 100644 index 000000000..39a4303aa Binary files /dev/null and b/forui/test/golden/calendar/day-picker/zinc-dark-max-rows.png differ diff --git a/forui/test/golden/calendar/day-picker/zinc-light-default.png b/forui/test/golden/calendar/day-picker/zinc-light-default.png new file mode 100644 index 000000000..499ba0104 Binary files /dev/null and b/forui/test/golden/calendar/day-picker/zinc-light-default.png differ diff --git a/forui/test/golden/calendar/day-picker/zinc-light-max-rows.png b/forui/test/golden/calendar/day-picker/zinc-light-max-rows.png new file mode 100644 index 000000000..eeca3a041 Binary files /dev/null and b/forui/test/golden/calendar/day-picker/zinc-light-max-rows.png differ diff --git a/forui/test/golden/calendar/month-picker/zinc-dark-default.png b/forui/test/golden/calendar/month-picker/zinc-dark-default.png new file mode 100644 index 000000000..ea656d363 Binary files /dev/null and b/forui/test/golden/calendar/month-picker/zinc-dark-default.png differ diff --git a/forui/test/golden/calendar/month-picker/zinc-light-default.png b/forui/test/golden/calendar/month-picker/zinc-light-default.png new file mode 100644 index 000000000..01504167d Binary files /dev/null and b/forui/test/golden/calendar/month-picker/zinc-light-default.png differ diff --git a/forui/test/golden/calendar/year-picker/zinc-dark-default.png b/forui/test/golden/calendar/year-picker/zinc-dark-default.png new file mode 100644 index 000000000..755b2533a Binary files /dev/null and b/forui/test/golden/calendar/year-picker/zinc-dark-default.png differ diff --git a/forui/test/golden/calendar/year-picker/zinc-dark-initial-date.png b/forui/test/golden/calendar/year-picker/zinc-dark-initial-date.png new file mode 100644 index 000000000..a2dc2edf0 Binary files /dev/null and b/forui/test/golden/calendar/year-picker/zinc-dark-initial-date.png differ diff --git a/forui/test/golden/calendar/year-picker/zinc-light-default.png b/forui/test/golden/calendar/year-picker/zinc-light-default.png new file mode 100644 index 000000000..78a0f9a03 Binary files /dev/null and b/forui/test/golden/calendar/year-picker/zinc-light-default.png differ diff --git a/forui/test/golden/calendar/year-picker/zinc-light-initial-date.png b/forui/test/golden/calendar/year-picker/zinc-light-initial-date.png new file mode 100644 index 000000000..cfb7ae928 Binary files /dev/null and b/forui/test/golden/calendar/year-picker/zinc-light-initial-date.png differ diff --git a/forui/test/src/widgets/calendar/calendar_controller_test.dart b/forui/test/src/widgets/calendar/calendar_controller_test.dart new file mode 100644 index 000000000..ed12189d0 --- /dev/null +++ b/forui/test/src/widgets/calendar/calendar_controller_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:forui/forui.dart'; + +void main() { + group('FCalendarSingleValueController', () { + test( + 'constructor throws error', + () => expect(() => FCalendarSingleValueController(DateTime.now()), throwsAssertionError), + ); + + for (final (date, expected) in [ + (DateTime.utc(2024, 5, 4), true), + (DateTime.utc(2024, 5, 5), false), + ]) { + test('contains(...) contains date', () { + final controller = FCalendarSingleValueController(DateTime.utc(2024, 5, 4)); + expect(controller.contains(date), expected); + }); + } + + for (final (initial, date, expected) in [ + (null, DateTime.utc(2024), DateTime.utc(2024)), + (null, DateTime.utc(2025), DateTime.utc(2025)), + (DateTime.utc(2024), DateTime.utc(2025), DateTime.utc(2025)), + (DateTime.utc(2024), DateTime.utc(2024), null), + ]) { + test('onPress(...)', () { + final controller = FCalendarSingleValueController(initial)..onPress(date); + expect(controller.value, expected); + }); + } + }); + + group('FCalendarMultiValueController', () { + for (final (date, expected) in [ + (DateTime.utc(2024), true), + (DateTime.utc(2025), false), + ]) { + test('contains(...)', () { + final controller = FCalendarMultiValueController({DateTime.utc(2024)}); + expect(controller.contains(date), expected); + }); + } + + for (final (initial, date, expected) in [ + ({DateTime.utc(2024)}, DateTime.utc(2024), {}), + ({}, DateTime.utc(2024), {DateTime.utc(2024)}), + ({DateTime.utc(2024)}, DateTime.utc(2025), {DateTime.utc(2024), DateTime.utc(2025)}), + ]) { + test('onPress(...)', () { + final controller = FCalendarMultiValueController(initial)..onPress(date); + expect(controller.value, expected); + }); + } + }); + + group('FCalendarSingleRangeController', () { + test( + 'constructor throws error', + () => expect(() => FCalendarSingleRangeController((DateTime(2025), DateTime(2024))), throwsAssertionError), + ); + + for (final (initial, date, expected) in [ + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2024), true), + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2025), true), + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2023), false), + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2026), false), + (null, DateTime.utc(2023), false), + ]) { + test('contains(...)', () { + final controller = FCalendarSingleRangeController(initial); + expect(controller.contains(date), expected); + }); + } + + for (final (initial, date, expected) in [ + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2024), null), + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2025), null), + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2023), (DateTime.utc(2023), DateTime.utc(2025))), + ((DateTime.utc(2024), DateTime.utc(2025)), DateTime.utc(2026), (DateTime.utc(2024), DateTime.utc(2026))), + ((DateTime.utc(2024), DateTime.utc(2027)), DateTime.utc(2025), (DateTime.utc(2024), DateTime.utc(2025))), + (null, DateTime.utc(2023), (DateTime.utc(2023), DateTime.utc(2023))), + ]) { + test('onPress(...)', () { + final controller = FCalendarSingleRangeController(initial)..onPress(date); + expect(controller.value, expected); + }); + } + }); +} diff --git a/forui/test/src/widgets/calendar/calendar_golden_test.dart b/forui/test/src/widgets/calendar/calendar_golden_test.dart new file mode 100644 index 000000000..4fed22c6e --- /dev/null +++ b/forui/test/src/widgets/calendar/calendar_golden_test.dart @@ -0,0 +1,178 @@ +@Tags(['golden']) +library; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:forui/forui.dart'; +import '../../test_scaffold.dart'; + +void main() { + final selected = { + DateTime.utc(2024, 7, 4), + DateTime.utc(2024, 7, 5), + DateTime.utc(2024, 7, 16), + DateTime.utc(2024, 7, 17), + DateTime.utc(2024, 7, 18), + }; + + group('FCalendar', () { + for (final (name, theme, background) in TestScaffold.themes) { + group('day picker', () { + testWidgets('default - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + background: background, + child: Padding( + padding: const EdgeInsets.all(16), + child: FCalendar( + controller: FCalendarMultiValueController(selected), + enabled: (date) => date != DateTime.utc(2024, 7, 2), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + ), + ), + ), + ); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + + await gesture.moveTo(tester.getCenter(find.text('8'))); + await tester.pumpAndSettle(); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('calendar/day-picker/$name-default.png'), + ); + }); + + testWidgets('max rows - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + background: background, + child: Padding( + padding: const EdgeInsets.all(16), + child: FCalendar( + controller: FCalendarMultiValueController(selected), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 6, 14), + ), + ), + ), + ); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('calendar/day-picker/$name-max-rows.png'), + ); + }); + }); + + group('month picker', () { + testWidgets('default - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + background: background, + child: Padding( + padding: const EdgeInsets.all(16), + child: FCalendar( + controller: FCalendarMultiValueController(selected), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + initialType: FCalendarPickerType.yearMonth, + ), + ), + ), + ); + + await tester.tap(find.text('2020')); + await tester.pumpAndSettle(); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + + await gesture.moveTo(tester.getCenter(find.text('Feb'))); + await tester.pumpAndSettle(); + + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('calendar/month-picker/$name-default.png'), + ); + }); + }); + + group('year picker', () { + testWidgets('default - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + background: background, + child: Padding( + padding: const EdgeInsets.all(16), + child: FCalendar( + controller: FCalendarMultiValueController(selected), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + initialType: FCalendarPickerType.yearMonth, + ), + ), + ), + ); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('calendar/year-picker/$name-default.png'), + ); + }); + + testWidgets('initial date different from today - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: theme, + background: background, + child: Padding( + padding: const EdgeInsets.all(16), + child: FCalendar( + controller: FCalendarMultiValueController(selected), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + initialDate: DateTime(1984, 4, 2), + initialType: FCalendarPickerType.yearMonth, + ), + ), + ), + ); + + final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(location: Offset.zero); + addTearDown(gesture.removePointer); + await tester.pump(); + + await gesture.moveTo(tester.getCenter(find.text('1991'))); + await tester.pumpAndSettle(); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('calendar/year-picker/$name-initial-date.png'), + ); + }); + }); + } + }); +} diff --git a/forui/test/src/widgets/calendar/calendar_test.dart b/forui/test/src/widgets/calendar/calendar_test.dart new file mode 100644 index 000000000..53c0c168c --- /dev/null +++ b/forui/test/src/widgets/calendar/calendar_test.dart @@ -0,0 +1,100 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:forui/forui.dart'; + +import '../../test_scaffold.dart'; + +void main() { + group('FCalendar', () { + group('previous button', () { + testWidgets('navigates to previous page', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: FThemes.zinc.light, + child: FCalendar( + controller: FCalendarMultiValueController(), + enabled: (date) => date != DateTime.utc(2024, 7, 2), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + ), + ), + ); + + expect(find.text('July 2024'), findsOneWidget); + + await tester.tap(find.byType(FButton).first); + await tester.pumpAndSettle(); + + expect(find.text('June 2024'), findsOneWidget); + }); + + testWidgets('did not navigate to previous page', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: FThemes.zinc.light, + child: FCalendar( + controller: FCalendarMultiValueController(), + enabled: (date) => date != DateTime.utc(2024, 7, 2), + start: DateTime(2024, 7), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + ), + ), + ); + + expect(find.text('July 2024'), findsOneWidget); + + await tester.tap(find.byType(FButton).first); + await tester.pumpAndSettle(); + + expect(find.text('June 2024'), findsNothing); + }); + }); + + group('next button', () { + testWidgets('navigates to next page', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: FThemes.zinc.light, + child: FCalendar( + controller: FCalendarMultiValueController(), + enabled: (date) => date != DateTime.utc(2024, 7, 2), + start: DateTime(1900, 1, 8), + end: DateTime(2024, 8, 10), + today: DateTime(2024, 7, 14), + ), + ), + ); + + expect(find.text('July 2024'), findsOneWidget); + + await tester.tap(find.byType(FButton).last); + await tester.pumpAndSettle(); + + expect(find.text('August 2024'), findsOneWidget); + }); + + testWidgets('did not navigate to next page', (tester) async { + await tester.pumpWidget( + TestScaffold( + data: FThemes.zinc.light, + child: FCalendar( + controller: FCalendarMultiValueController(), + enabled: (date) => date != DateTime.utc(2024, 7, 2), + start: DateTime(2024), + end: DateTime(2024, 7, 10), + today: DateTime(2024, 7, 14), + ), + ), + ); + + expect(find.text('July 2024'), findsOneWidget); + + await tester.tap(find.byType(FButton).first); + await tester.pumpAndSettle(); + + expect(find.text('August 2024'), findsNothing); + }); + }); + }); +} diff --git a/samples/lib/main.dart b/samples/lib/main.dart index 3b5b40075..75af89b89 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -49,6 +49,18 @@ class _AppRouter extends $_AppRouter { path: '/button/icon', page: ButtonIconRoute.page, ), + AutoRoute( + path: '/calendar/default', + page: CalendarRoute.page, + ), + AutoRoute( + path: '/calendar/multi-value', + page: MultiValueCalendarRoute.page, + ), + AutoRoute( + path: '/calendar/single-range', + page: SingleRangeCalendarRoute.page, + ), AutoRoute( path: '/card/default', page: CardRoute.page, diff --git a/samples/lib/widgets/calendar.dart b/samples/lib/widgets/calendar.dart new file mode 100644 index 000000000..cde955a5f --- /dev/null +++ b/samples/lib/widgets/calendar.dart @@ -0,0 +1,46 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:forui/forui.dart'; +import 'package:forui_samples/sample_scaffold.dart'; + +@RoutePage() +class CalendarPage extends SampleScaffold { + CalendarPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => FCalendar( + controller: FCalendarSingleValueController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ); +} + +@RoutePage() +class MultiValueCalendarPage extends SampleScaffold { + MultiValueCalendarPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => FCalendar( + controller: FCalendarMultiValueController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ); +} + +@RoutePage() +class SingleRangeCalendarPage extends SampleScaffold { + SingleRangeCalendarPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => FCalendar( + controller: FCalendarSingleRangeController(), + start: DateTime.utc(2024), + end: DateTime.utc(2030), + ); +} diff --git a/samples/pubspec.lock b/samples/pubspec.lock index 4fac45f3b..9968df5e8 100644 --- a/samples/pubspec.lock +++ b/samples/pubspec.lock @@ -479,10 +479,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a + sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" url: "https://pub.dev" source: hosted - version: "2.2.6" + version: "2.2.7" path_provider_foundation: dependency: transitive description: @@ -511,10 +511,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -744,14 +744,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - win32: - dependency: transitive - description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 - url: "https://pub.dev" - source: hosted - version: "5.5.1" xdg_directories: dependency: transitive description: