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: