diff --git a/forui/example/lib/sandbox.dart b/forui/example/lib/sandbox.dart index c9dd5b863..24efaecb3 100644 --- a/forui/example/lib/sandbox.dart +++ b/forui/example/lib/sandbox.dart @@ -34,7 +34,7 @@ class _SandboxState extends State { const FTextField.password(), FLineCalendar( controller: FCalendarController.date( - initialSelection: DateTime(2024, 10, 13).toUtc(), + initialSelection: DateTime.utc(2024, 9, 13), ), ), const SizedBox(height: 20), diff --git a/forui/lib/src/widgets/line_calendar/line_calendar.dart b/forui/lib/src/widgets/line_calendar/line_calendar.dart index f304edebe..9543cbe15 100644 --- a/forui/lib/src/widgets/line_calendar/line_calendar.dart +++ b/forui/lib/src/widgets/line_calendar/line_calendar.dart @@ -2,14 +2,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:forui/src/widgets/line_calendar/line_calendar_controller.dart'; +import 'package:forui/forui.dart'; import 'package:forui/src/widgets/line_calendar/line_calendar_tile.dart'; import 'package:meta/meta.dart'; import 'package:sugar/sugar.dart'; -import 'package:forui/forui.dart' hide FLineCalendar, FLineCalendarContentStyle, FLineCalendarStyle; - const _textSpacing = 2.0; /// A calendar that can be scrolled horizontally. @@ -76,9 +74,6 @@ class _FLineCalendarState extends State { final offset = (value.difference(widget.start.toLocalDate()).inDays - 2) * _size + _style.itemPadding; _controller = ScrollController(initialScrollOffset: offset); - final textDirection = Directionality.of(context); - widget.controller.addListener(() => _onDateChange(textDirection)); - super.didChangeDependencies(); } @@ -90,33 +85,43 @@ class _FLineCalendarState extends State { return dateTextSize + dayTextSize + _textSpacing + (style.content.verticalPadding * 2); } - void _onDateChange(TextDirection textDirection) { - setState(() { - //TODO: localizations. - SemanticsService.announce(widget.controller.value.toString(), textDirection); - }); - } - @override Widget build(BuildContext context) => SizedBox( height: _size, - child: ListView.builder( - controller: _controller, - scrollDirection: Axis.horizontal, - padding: EdgeInsets.zero, - itemExtent: _size, - itemBuilder: (context, index) { - final date = widget.start.add(Duration(days: index)); - return Container( - padding: EdgeInsets.symmetric(horizontal: _style.itemPadding), - child: FlineCalendarTile( - style: _style, - controller: widget.controller, - date: date, - isToday: widget.today == date, - ), - ); + child: Focus( + autofocus: true, + onKeyEvent: (node, event) { + if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.arrowRight) { + widget.controller.value = widget.controller.value?.add(const Duration(days: 1)); + _controller.animateTo(_controller.offset + _size, + duration: const Duration(milliseconds: 100), curve: Curves.easeInOut); + return KeyEventResult.handled; + } else if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.arrowLeft) { + widget.controller.value = widget.controller.value?.subtract(const Duration(days: 1)); + _controller.animateTo(_controller.offset - _size, + duration: const Duration(milliseconds: 100), curve: Curves.easeInOut); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; }, + child: ListView.builder( + controller: _controller, + scrollDirection: Axis.horizontal, + padding: EdgeInsets.zero, + itemExtent: _size, + itemBuilder: (context, index) { + final date = widget.start.add(Duration(days: index)); + return Container( + padding: EdgeInsets.symmetric(horizontal: _style.itemPadding), + child: FlineCalendarTile( + style: _style, + controller: widget.controller, + date: date, + isToday: widget.today == date, + ), + ); + }, + ), ), ); diff --git a/forui/lib/src/widgets/line_calendar/line_calendar_controller.dart b/forui/lib/src/widgets/line_calendar/line_calendar_controller.dart deleted file mode 100644 index be99bc605..000000000 --- a/forui/lib/src/widgets/line_calendar/line_calendar_controller.dart +++ /dev/null @@ -1,122 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:sugar/sugar.dart'; - -final _default = (DateTime(1900).toUtc(), null); - -bool _true(DateTime _) => true; - -/// A controller for the FLineCalendar widget. -class FLineCalendarController extends ValueNotifier { - /// The [_calendarRange] parameter defines the range of dates that can be displayed in the calendar. - /// Both the start and end dates of the range is inclusive. The selected dates are always in UTC timezone and - /// truncated to the nearest day. - final (DateTime, DateTime?) _calendarRange; - final DateTime _today; - final Predicate _selectable; - - /// The scroll controller. - final ScrollController scrollController; - - /// Creates a [FLineCalendarController]. - /// - /// [selectable] will always return true if not given. - /// - /// ## Contract - /// Throws [AssertionError] if: - /// * if [initialSelection] is not in UTC timezone. - /// * the dates in [calendarRange] are not in UTC timezone. - /// * the end date is less than start date. - FLineCalendarController({ - (DateTime start, DateTime? end)? calendarRange, - DateTime? today, - Predicate? selectable, - DateTime? initialSelection, - ScrollController? scrollController, - }) : assert(initialSelection?.isUtc ?? true, 'value must be in UTC timezone'), - assert( - calendarRange == null || (calendarRange.$1.isUtc && (calendarRange.$2?.isUtc ?? true)), - 'value must be in UTC timezone', - ), - assert( - calendarRange == null || - calendarRange.$2 == null || - calendarRange.$1.isBefore(calendarRange.$2!) || - calendarRange.$1.isAtSameMomentAs(calendarRange.$2!), - 'end date must be greater than or equal to start date', - ), - _calendarRange = calendarRange ?? _default, - _today = today ?? DateTime.now(), - _selectable = selectable ?? _true, - scrollController = scrollController ?? ScrollController(), - super(initialSelection ?? DateTime.now().toUtc()); - - /// Returns a new instance of [FLineCalendarController] with the same properties, - /// but with its [ScrollController] initialized to a specific scroll offset. - /// - /// The [size], [itemPadding], and [position] parameters are used to calculate - /// the initial scroll offset for the [ScrollController]. - /// - /// * [size]: The size of each calendar item. - /// * [itemPadding]: The padding between calendar items. - /// * [position]: The position of the selected date within the visible items. - /// - /// This method is useful for creating a controller that starts with a specific - /// scroll position, typically to ensure the selected date is visible. - FLineCalendarController withInitialScrollOffset( - double size, - double itemPadding, { - int position = 3, - }) => - FLineCalendarController( - calendarRange: (start, end), - today: _today, - selectable: _selectable, - initialSelection: value, - scrollController: ScrollController( - initialScrollOffset: _selectedDateOffset(size, itemPadding, position), - debugLabel: scrollController.debugLabel, - keepScrollOffset: scrollController.keepScrollOffset, - onAttach: scrollController.onAttach, - onDetach: scrollController.onDetach, - ), - ); - - /// Returns true if the given [date] can be selected/unselected. - /// - /// [date] should always in UTC timezone and truncated to the nearest day. - /// - /// ## Note - /// It is unsafe for this function to have side effects since it may be called more than once for a single date. As it - /// is called frequently, it should not be computationally expensive. - bool selectable(DateTime date) => _selectable(date); - - /// Returns true if the given [date] is selected. - /// - /// [date] should always in UTC timezone and truncated to the nearest day. - bool selected(DateTime date) => value.toLocalDate() == date.toLocalDate(); - - /// Selects the given [date]. - /// - /// [date] should always in UTC timezone and truncated to the nearest day. - void select(DateTime date) { - if (value.toLocalDate() == date.toLocalDate()) { - return; - } - value = date; - } - - double _selectedDateOffset(double size, double itemPadding, int position) => - (value.toLocalDate().difference(start.toLocalDate()).inDays - (position - 1)) * size + itemPadding; - - /// Returns `true` if the given date is today. - bool isToday(DateTime date) => date.toLocalDate() == _today.toLocalDate(); - - /// The first date in this calendar carousel. Defaults to 1st January 1900. - DateTime get start => _calendarRange.$1; - - /// The final date in this calendar carousel. - DateTime? get end => _calendarRange.$2; - - /// Returns the current date. It is truncated to the nearest date. Defaults to the [DateTime.now] - DateTime get today => _today; -} diff --git a/forui/test/src/widgets/line_calendar_golden_test.dart b/forui/test/src/widgets/line_calendar_golden_test.dart index aa48fabf8..281a12287 100644 --- a/forui/test/src/widgets/line_calendar_golden_test.dart +++ b/forui/test/src/widgets/line_calendar_golden_test.dart @@ -1,13 +1,14 @@ @Tags(['golden']) library; +import 'dart:ui'; + import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:forui/forui.dart'; import 'package:forui/src/widgets/line_calendar/line_calendar.dart'; -import 'package:forui/src/widgets/line_calendar/line_calendar_controller.dart'; import '../test_scaffold.dart'; void main() { @@ -32,7 +33,7 @@ void main() { for (final (name, theme, _) in TestScaffold.themes) { for (final (lineCalendar, controller) in [ - ('default', FLineCalendarController(today: DateTime(2024, 10, 20))), + ('default', FCalendarController.date(initialSelection: DateTime.utc(2024, 10, 20))), ]) { testWidgets('$name - $lineCalendar', (tester) async { await tester.pumpWidget( @@ -41,15 +42,58 @@ void main() { child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: FLineCalendar(controller: FCalendarController.date()), + child: FLineCalendar(controller: controller), + ), + ), + ), + ); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('line_calendar/$name-$lineCalendar/default.png'), + ); + }); + + testWidgets('new date selected - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + theme: theme, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: FLineCalendar(controller: controller), + ), + ), + ), + ); + + await tester.tap(find.text('24')); + await tester.pumpAndSettle(); + + await expectLater( + find.byType(TestScaffold), + matchesGoldenFile('line_calendar/$name-$lineCalendar/new-date.png'), + ); + }); + + testWidgets('unselected - $name', (tester) async { + await tester.pumpWidget( + TestScaffold( + theme: theme, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: FLineCalendar(controller: controller), ), ), ), ); + await tester.tap(find.text('20')); + await tester.pumpAndSettle(); await expectLater( find.byType(TestScaffold), - matchesGoldenFile('line_calendar/$name-$lineCalendar.png'), + matchesGoldenFile('line_calendar/$name-$lineCalendar/unselected.png'), ); }); }