diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab17e4383..d52aeb35e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,16 @@ # Contributing to Forui +Before starting work on a pull request, please check if a similar issue already exists, or create one to discuss the +proposed changes. + +This helps to: + +* Ensure that the proposed changes align with the project's goals and direction. +* Avoid duplicate efforts by informing other contributors about ongoing work. +* Provide a platform for maintainers and the community to offer feedback and suggestions. + +Doing so saves time and effort by identifying potential problems early in the development process. + ## Design Guidelines ### Be agnostic about state management diff --git a/docs/pages/docs/calendar.mdx b/docs/pages/docs/calendar.mdx index d3f2f3762..9cfcdd3d3 100644 --- a/docs/pages/docs/calendar.mdx +++ b/docs/pages/docs/calendar.mdx @@ -4,8 +4,11 @@ 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. +The calendar pages are designed to be navigable through swipe gestures on mobile platforms, allowing left and right +swipes to transition between pages. + +A [FCalendarController](https://pub.dev/documentation/forui/latest/forui.widgets/FCalendarController-class.html) is used +to customize the date selection behavior. @@ -14,7 +17,7 @@ to transition between pages. ```dart FCalendar( - controller: FCalendarSingleRangeController(), + controller: FCalendarValueController(initialSelection: selected), start: DateTime.utc(2000), end: DateTime.utc(2030), ); @@ -28,13 +31,15 @@ to transition between pages. ```dart FCalendar( - controller: FCalendarSingleRangeController(), + controller: FCalendarValueController( + initialSelection: DateTime.utc(2024, 9, 13), + selectable: (date) => allowedDates.contains(date), + ), 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), + initialType = FCalendarPickerType.yearMonth, + initialMonth = DateTime.utc(2024, 9), onMonthChange: (date) => print(date), onPress: (date) => print(date), onLongPress: (date) => print(date), @@ -58,7 +63,7 @@ FCalendar( -### Multiple Dates +### Multiple Dates with Initial Selections @@ -66,26 +71,51 @@ FCalendar( ```dart FCalendar( - controller: FCalendarMultiValueController(), + controller: FCalendarMultiValueController( + initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, + ), start: DateTime.utc(2000), + today: DateTime.utc(2024, 7, 15), end: DateTime.utc(2030), ); ``` -### Single Range +### Unselectable Dates + + + + + ```dart + FCalendar( + controller: FCalendarMultiValueController( + initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, + selectable: (date) => !{DateTime.utc(2024, 7, 18), DateTime.utc(2024, 7, 19)}.contains(date), + ), + start: DateTime.utc(2000), + today: DateTime.utc(2024, 7, 15), + end: DateTime.utc(2030), + ); + ``` + + + +### Range Selection with Initial Range - + ```dart FCalendar( - controller: FCalendarSingleRangeController(), + controller: FCalendarRangeController( + initialSelection: (DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)), + ), start: DateTime.utc(2000), + today: DateTime.utc(2024, 7, 15), end: DateTime.utc(2030), - ) + ); ``` diff --git a/forui/CHANGELOG.md b/forui/CHANGELOG.md index e62e58a6c..dcb51346f 100644 --- a/forui/CHANGELOG.md +++ b/forui/CHANGELOG.md @@ -2,8 +2,30 @@ ### Additions * Add `FAvatar` +* **Breaking:** Add `FCalendarEntryStyle.focusedBorderColor`. This only affects users that customized `FCalendarEntryStyle`. * Add `FResizable` +### Changes +* Change number of years displayed per page in `FCalendar` from 12 to 15. +* **Breaking:** Move `FCalendar.enabled` to `FCalendarController.canSelect(...)`. + +* **Breaking:** Rename `FCalendarController.contains(...)` to `FCalendarController.selected(...)`. +* **Breaking:** Rename `FCalendarController.onPress(...)` to `FCalendarController.select(...)`. + +* **Breaking:** Rename `FCalendarEntryStyle.focusedBackgroundColor` to `FCalendarEntryStyle.hoveredBackgroundColor`. + This only affects users that customized `FCalendarEntryStyle`. + +* **Breaking:** Rename `FCalendarEntryStyle.focusedTextStyle` to `FCalendarEntryStyle.hoveredTextStyle`. + This only affects users that customized `FCalendarEntryStyle`. + +* **Breaking:** Rename `FCalendarSingleValueController` to `FCalendarValueController`. + +* **Breaking:** Rename `FCalendarSingleRangeController` to `FCalendarRangeController`. + +### Fixes +* Fix `FCalendar` dates not being toggleable using `Enter` key. +* Fix `FCalendar` dates sometimes not being navigable using arrow keys. + ## 0.3.0 @@ -25,16 +47,19 @@ ### Fixes * Fix broken images in README.md (yet again). + ## 0.2.0+2 ### Fixes * Fix broken images in README.md. + ## 0.2.0+1 ### Fixes * Fix broken images in README.md. + ## 0.2.0 ### Additions @@ -58,6 +83,7 @@ * **Breaking** `FButton.prefixIcon` and `FButton.suffixIcon` have been renamed to `FButton.prefix` and `FButton.suffix`. * Fix padding inconsistencies in `FCard` and `FDialog`. + ## 0.1.0 * Initial release! 🚀 diff --git a/forui/example/lib/example.dart b/forui/example/lib/example.dart index 3b41b8274..816a04dcb 100644 --- a/forui/example/lib/example.dart +++ b/forui/example/lib/example.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:forui/forui.dart'; + class Example extends StatefulWidget { const Example({super.key}); @@ -14,5 +16,14 @@ class _ExampleState extends State { } @override - Widget build(BuildContext context) => const Placeholder(); + Widget build(BuildContext context) => Column( + children: [ + FCalendar( + controller: FCalendarValueController(), + initialType: FCalendarPickerType.yearMonth, + start: DateTime.utc(2000), + end: DateTime.utc(2030), + ), + ], + ); } diff --git a/forui/example/lib/main.dart b/forui/example/lib/main.dart index 5c0544cc6..149fd800d 100644 --- a/forui/example/lib/main.dart +++ b/forui/example/lib/main.dart @@ -32,7 +32,7 @@ class _ApplicationState extends State { ), ], ), - content: const Example(), + content: child!, footer: FBottomNavigationBar( index: index, onChange: (index) => setState(() => this.index = index), diff --git a/forui/lib/src/foundation/tappable.dart b/forui/lib/src/foundation/tappable.dart index 3744c2c84..6aa41416e 100644 --- a/forui/lib/src/foundation/tappable.dart +++ b/forui/lib/src/foundation/tappable.dart @@ -1,11 +1,16 @@ import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; +// TODO: Remove redundant comment when flutter fixes its lint issue. +/// +@internal +typedef FTappableState = ({bool focused, bool hovered}); + @internal class FTappable extends StatefulWidget { - final bool enabled; final String? semanticLabel; final bool selected; final bool excludeSemantics; @@ -14,7 +19,7 @@ class FTappable extends StatefulWidget { final ValueChanged? onFocusChange; final VoidCallback? onPress; final VoidCallback? onLongPress; - final ValueWidgetBuilder builder; + final ValueWidgetBuilder builder; final Widget? child; factory FTappable.animated({ @@ -26,7 +31,7 @@ class FTappable extends StatefulWidget { ValueChanged? onFocusChange, VoidCallback? onPress, VoidCallback? onLongPress, - ValueWidgetBuilder? builder, + ValueWidgetBuilder? builder, Widget? child, Key? key, }) = _AnimatedTappable; @@ -40,12 +45,13 @@ class FTappable extends StatefulWidget { this.onFocusChange, this.onPress, this.onLongPress, - ValueWidgetBuilder? builder, + ValueWidgetBuilder? builder, this.child, super.key, }) : assert(builder != null || child != null, 'Either builder or child must be provided.'), - builder = builder ?? ((_, __, child) => child!), - enabled = onPress != null || onLongPress != null; + builder = builder ?? ((_, __, child) => child!); + + bool get enabled => onPress != null || onLongPress != null; @override State createState() => _FTappableState(); @@ -80,33 +86,51 @@ class _FTappableState extends State with SingleTickerProviderStateMix bool _hovered = false; @override - Widget build(BuildContext context) => Semantics( - enabled: widget.enabled, - label: widget.semanticLabel, - container: true, - button: true, - selected: widget.selected, - excludeSemantics: widget.excludeSemantics, - child: Focus( - autofocus: widget.autofocus, - focusNode: widget.focusNode, - onFocusChange: (focused) { - setState(() => _focused = focused); - widget.onFocusChange?.call(focused); - }, - child: MouseRegion( - cursor: widget.enabled ? SystemMouseCursors.click : MouseCursor.defer, - onEnter: (_) => setState(() => _hovered = true), - onExit: (_) => setState(() => _hovered = false), - child: _child, - ), + Widget build(BuildContext context) { + final tappable = Semantics( + enabled: widget.enabled, + label: widget.semanticLabel, + container: true, + button: true, + selected: widget.selected, + excludeSemantics: widget.excludeSemantics, + child: Focus( + autofocus: widget.autofocus, + focusNode: widget.focusNode, + onFocusChange: (focused) { + setState(() => _focused = focused); + widget.onFocusChange?.call(focused); + }, + child: MouseRegion( + cursor: widget.enabled ? SystemMouseCursors.click : MouseCursor.defer, + onEnter: (_) => setState(() => _hovered = true), + onExit: (_) => setState(() => _hovered = false), + child: _child, ), - ); + ), + ); + + if (widget.onPress == null) { + return tappable; + } + + return Shortcuts( + shortcuts: const { + SingleActivator(LogicalKeyboardKey.enter): ActivateIntent(), + }, + child: Actions( + actions: { + ActivateIntent: CallbackAction(onInvoke: (_) => widget.onPress!()), + }, + child: tappable, + ), + ); + } Widget get _child => GestureDetector( onTap: widget.onPress, onLongPress: widget.onLongPress, - child: widget.builder(context, _focused || _hovered, widget.child), + child: widget.builder(context, (focused: _focused, hovered: _hovered), widget.child), ); } @@ -156,7 +180,7 @@ class _AnimatedTappableState extends _FTappableState { _controller.forward(); }, onLongPress: widget.onLongPress, - child: widget.builder(context, _focused || _hovered, widget.child), + child: widget.builder(context, (focused: _focused, hovered: _hovered), widget.child), ), ); diff --git a/forui/lib/src/widgets/calendar/calendar.dart b/forui/lib/src/widgets/calendar/calendar.dart index 80471c381..094a3d2ae 100644 --- a/forui/lib/src/widgets/calendar/calendar.dart +++ b/forui/lib/src/widgets/calendar/calendar.dart @@ -21,12 +21,14 @@ export 'year_month_picker.dart' show FCalendarYearMonthPickerStyle; /// 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. /// +/// All [DateTime]s are in UTC timezone. A [FCalendarController] is used to customize the date selection behavior. +/// [DateTime]s outside [start] and [end] are unselectable regardless of the [FCalendarController] used. +/// /// See: /// * https://forui.dev/docs/calendar for working examples. -/// * [FCalendarDayStyle] for customizing a card's appearance. +/// * for customizing a calendar's date selection behavior. +/// * [FCalendarDayStyle] for customizing a calendar's appearance. class FCalendar extends StatelessWidget { - static bool _true(DateTime _) => true; - /// The style. Defaults to [FThemeData.calendarStyle]. final FCalendarStyle? style; @@ -48,11 +50,6 @@ class FCalendar extends StatelessWidget { /// The current date. It is truncated to the nearest date. Defaults to the [DateTime.now]. final DateTime today; - /// A predicate that determines if a date can be selected. It may be called more than once for a single date. - /// - /// Defaults to returning true for all dates. - final Predicate enabled; - /// A callback for when the displayed month changes. final ValueChanged? onMonthChange; @@ -61,29 +58,29 @@ class FCalendar extends StatelessWidget { /// A callback for when a date in a [FCalendarPickerType.day] picker is long pressed. final ValueChanged? onLongPress; + final ValueNotifier _type; final ValueNotifier _month; /// Creates a [FCalendar]. /// - /// [initialDate] defaults to [today]. It is truncated to the nearest date. + /// [initialMonth] defaults to [today]. It is truncated to the nearest date. FCalendar({ required this.controller, required this.start, required this.end, this.style, - this.enabled = _true, this.onMonthChange, this.onPress, this.onLongPress, FCalendarPickerType initialType = FCalendarPickerType.day, DateTime? today, - DateTime? initialDate, + DateTime? initialMonth, super.key, }) : assert(start.toLocalDate() < end.toLocalDate(), 'end date must be greater than start date'), today = today ?? DateTime.now(), _type = ValueNotifier(initialType), - _month = ValueNotifier((initialDate ?? today ?? DateTime.now()).toLocalDate().truncate(to: DateUnit.months)); + _month = ValueNotifier((initialMonth ?? today ?? DateTime.now()).toLocalDate().truncate(to: DateUnit.months)); @override Widget build(BuildContext context) { @@ -115,15 +112,15 @@ class FCalendar extends StatelessWidget { end: end.toLocalDate(), today: today.toLocalDate(), initial: _month.value, - enabled: (date) => enabled(date.toNative()), - selected: (date) => controller.contains(date.toNative()), + selectable: (date) => controller.selectable(date.toNative()), + selected: (date) => controller.selected(date.toNative()), onMonthChange: (date) { _month.value = date; onMonthChange?.call(date.toNative()); }, onPress: (date) { final native = date.toNative(); - controller.onPress(native); + controller.select(native); onPress?.call(native); }, onLongPress: (date) => onLongPress?.call(date.toNative()), @@ -157,7 +154,6 @@ class FCalendar extends StatelessWidget { ..add(DiagnosticsProperty('start', start)) ..add(DiagnosticsProperty('end', end)) ..add(DiagnosticsProperty('today', today)) - ..add(DiagnosticsProperty('enabled', enabled)) ..add(DiagnosticsProperty('onMonthChange', onMonthChange)) ..add(DiagnosticsProperty('onPress', onPress)) ..add(DiagnosticsProperty('onLongPress', onLongPress)); diff --git a/forui/lib/src/widgets/calendar/calendar_controller.dart b/forui/lib/src/widgets/calendar/calendar_controller.dart index 065fc6ae5..c5d522b45 100644 --- a/forui/lib/src/widgets/calendar/calendar_controller.dart +++ b/forui/lib/src/widgets/calendar/calendar_controller.dart @@ -2,60 +2,95 @@ import 'package:flutter/widgets.dart'; import 'package:sugar/sugar.dart'; -import 'package:forui/forui.dart'; +bool _true(DateTime _) => true; /// A controller that controls date selection in a calendar. /// +/// The [DateTime]s are always in UTC timezone and truncated to the nearest day. +/// /// This class should be extended to customize date selection. By default, the following controllers are provided: -/// * [FCalendarSingleValueController] for selecting a single date. +/// * [FCalendarValueController] for selecting a single date. /// * [FCalendarMultiValueController] for selecting multiple date. -/// * [FCalendarSingleRangeController] for selecting a single range. +/// * [FCalendarRangeController] for selecting a single range. abstract class FCalendarController extends ValueNotifier { /// Creates a [FCalendarController] with the given initial [value]. FCalendarController(super._value); - /// Called when the given [date] in a [FCalendarPickerType.day] picker is pressed. + /// Returns true if the given [date] can be selected/unselected. + /// + /// [date] should always in UTC timezone and truncated to the nearest day. /// - /// [date] is always in UTC timezone and truncated to the nearest date. - void onPress(DateTime date); + /// ## 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); /// Returns true if the given [date] is selected. - bool contains(DateTime date); + /// + /// [date] should always in UTC timezone and truncated to the nearest day. + bool selected(DateTime date); + + /// Selects the given [date]. + /// + /// [date] should always in UTC timezone and truncated to the nearest day. + void select(DateTime date); } -/// A date selection controller that allows only a single date to be selected. +/// A controller that allows only a single date to be selected. /// -/// The selected date is always in UTC timezone and truncated to the nearest date. -final class FCalendarSingleValueController extends FCalendarController { - /// Creates a [FCalendarSingleValueController] with the given initial [value]. +/// The [DateTime]s are always in UTC timezone and truncated to the nearest date. +class FCalendarValueController extends FCalendarController { + final Predicate _selectable; + + /// Creates a [FCalendarValueController] with the given initially selected date. + /// + /// [selectable] will always return true if not given. /// /// ## Contract - /// Throws [AssertionError] if the given [value] is not in UTC timezone. - FCalendarSingleValueController([super.value]) : assert(value?.isUtc ?? true, 'value must be in UTC timezone'); + /// Throws [AssertionError] if [initialSelection] is not in UTC timezone. + FCalendarValueController({ + DateTime? initialSelection, + Predicate? selectable, + }) : assert(initialSelection?.isUtc ?? true, 'value must be in UTC timezone'), + _selectable = selectable ?? _true, + super(initialSelection); @override - bool contains(DateTime date) => value?.toLocalDate() == date.toLocalDate(); + bool selectable(DateTime date) => _selectable(date); @override - void onPress(DateTime date) => value = value?.toLocalDate() == date.toLocalDate() ? null : date; + bool selected(DateTime date) => value?.toLocalDate() == date.toLocalDate(); + + @override + void select(DateTime date) => value = value?.toLocalDate() == date.toLocalDate() ? null : date; } -/// A date selection controller that allows multiple dates to be selected. +/// A controller that allows multiple dates to be selected. The maximum number of dates that can be selected is +/// determined by [max]. /// -/// The selected dates are always in UTC timezone and truncated to the nearest date. -final class FCalendarMultiValueController extends FCalendarController> { +/// The [DateTime]s are always in UTC timezone and truncated to the nearest day. +class FCalendarMultiValueController extends FCalendarController> { + final Predicate _selectable; + /// Creates a [FCalendarMultiValueController] with the given initial [value]. /// /// ## Contract - /// Throws [AssertionError] if the given dates in [value] is not in UTC timezone. - FCalendarMultiValueController([super.value = const {}]) - : assert(value.every((d) => d.isUtc), 'dates must be in UTC timezone'); + /// Throws [AssertionError] if the dates in [initialSelections] are not in UTC timezone. + FCalendarMultiValueController({ + Set initialSelections = const {}, + Predicate? canSelect, + }) : assert(initialSelections.every((d) => d.isUtc), 'dates must be in UTC timezone'), + _selectable = canSelect ?? _true, + super(initialSelections); + + @override + bool selectable(DateTime date) => _selectable(date); @override - bool contains(DateTime date) => value.contains(date); + bool selected(DateTime date) => value.contains(date); @override - void onPress(DateTime date) { + void select(DateTime date) { final copy = {...value}; value = copy..toggle(date); } @@ -64,23 +99,37 @@ final class FCalendarMultiValueController extends FCalendarController { - /// Creates a [FCalendarSingleRangeController] with the given initial [value]. +/// to the nearest day. Unselectable dates within the selected range are selected regardless. +class FCalendarRangeController extends FCalendarController<(DateTime, DateTime)?> { + final Predicate _selectable; + + /// Creates a [FCalendarRangeController] with the given initial [value]. /// /// ## Contract /// Throws [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'), + FCalendarRangeController({ + (DateTime, DateTime)? initialSelection, + Predicate? canSelect, + }) : assert( + initialSelection == null || (initialSelection.$1.isUtc && initialSelection.$2.isUtc), + 'value must be in UTC timezone', + ), assert( - value == null || (value.$1.isBefore(value.$2) || value.$1.isAtSameMomentAs(value.$2)), + initialSelection == null || + (initialSelection.$1.isBefore(initialSelection.$2) || + initialSelection.$1.isAtSameMomentAs(initialSelection.$2)), 'end date must be greater than or equal to start date', - ); + ), + _selectable = canSelect ?? _true, + super(initialSelection); + + @override + bool selectable(DateTime date) => _selectable(date); @override - bool contains(DateTime date) { + bool selected(DateTime date) { if (value case (final first, final last)) { final current = date.toLocalDate(); return first.toLocalDate() <= current && current <= last.toLocalDate(); @@ -90,7 +139,7 @@ final class FCalendarSingleRangeController extends FCalendarController<(DateTime } @override - void onPress(DateTime date) { + void select(DateTime date) { if (value == null) { value = (date, date); return; diff --git a/forui/lib/src/widgets/calendar/day/day_picker.dart b/forui/lib/src/widgets/calendar/day/day_picker.dart index 6df7b9152..3e8dee733 100644 --- a/forui/lib/src/widgets/calendar/day/day_picker.dart +++ b/forui/lib/src/widgets/calendar/day/day_picker.dart @@ -19,7 +19,7 @@ class DayPicker extends StatefulWidget { final LocalDate month; final LocalDate today; final LocalDate? focused; - final Predicate enabled; + final Predicate selectable; final Predicate selected; final ValueChanged onPress; final ValueChanged onLongPress; @@ -29,7 +29,7 @@ class DayPicker extends StatefulWidget { required this.month, required this.today, required this.focused, - required this.enabled, + required this.selectable, required this.selected, required this.onPress, required this.onLongPress, @@ -47,8 +47,8 @@ class DayPicker extends StatefulWidget { ..add(DiagnosticsProperty('month', month)) ..add(DiagnosticsProperty('today', today)) ..add(DiagnosticsProperty('focused', focused)) - ..add(DiagnosticsProperty('enabledPredicate', enabled, ifNull: 'all enabled')) - ..add(DiagnosticsProperty('selectedPredicate', selected, ifNull: 'none selected')) + ..add(DiagnosticsProperty('selectable', selectable)) + ..add(DiagnosticsProperty('selected', selected)) ..add(DiagnosticsProperty('onPress', onPress)) ..add(DiagnosticsProperty('onLongPress', onLongPress)); } @@ -112,7 +112,7 @@ class _DayPickerState extends State { focusNode: focusNode, current: date.month == widget.month.month, today: date == widget.today, - enabled: widget.enabled, + selectable: widget.selectable, selected: widget.selected, onPress: widget.onPress, onLongPress: widget.onLongPress, @@ -176,11 +176,11 @@ final class FCalendarDayPickerStyle with Diagnosticable { /// The text style for the day of th week headers. final TextStyle headerTextStyle; - /// The styles of the current month on display and the enclosing months, when enabled. - final ({FCalendarDayStyle current, FCalendarDayStyle enclosing}) enabledStyles; + /// The styles of selectable dates in the current month on display and the enclosing months. + final ({FCalendarDayStyle current, FCalendarDayStyle enclosing}) selectableStyles; - /// The styles of the current month on display and the enclosing months, when disabled. - final ({FCalendarDayStyle current, FCalendarDayStyle enclosing}) disabledStyles; + /// The styles of unselectable dates in the current month on display and the enclosing months. + final ({FCalendarDayStyle current, FCalendarDayStyle enclosing}) unselectableStyles; /// The starting day of the week. Defaults to the current locale's preferred starting day of the week if null. /// @@ -195,8 +195,8 @@ final class FCalendarDayPickerStyle with Diagnosticable { /// Creates a [FCalendarDayPickerStyle]. const FCalendarDayPickerStyle({ required this.headerTextStyle, - required this.enabledStyles, - required this.disabledStyles, + required this.selectableStyles, + required this.unselectableStyles, this.startDayOfWeek, }) : assert( startDayOfWeek == null || (DateTime.monday <= startDayOfWeek && startDayOfWeek <= DateTime.sunday), @@ -213,28 +213,32 @@ final class FCalendarDayPickerStyle with Diagnosticable { selectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.primaryForeground, textStyle: mutedTextStyle, + focusedBorderColor: colorScheme.primaryForeground, radius: const Radius.circular(4), ), unselectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: mutedTextStyle, + focusedBorderColor: colorScheme.background, radius: const Radius.circular(4), ), ); return FCalendarDayPickerStyle( headerTextStyle: typography.xs.copyWith(color: colorScheme.mutedForeground), - enabledStyles: ( + selectableStyles: ( current: FCalendarDayStyle( selectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.foreground, textStyle: typography.sm.copyWith(color: colorScheme.background, fontWeight: FontWeight.w500), + focusedBorderColor: colorScheme.foreground, radius: const Radius.circular(4), ), unselectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: textStyle, - focusedBackgroundColor: colorScheme.secondary, + hoveredBackgroundColor: colorScheme.secondary, + focusedBorderColor: colorScheme.foreground, radius: const Radius.circular(4), ), ), @@ -242,17 +246,19 @@ final class FCalendarDayPickerStyle with Diagnosticable { selectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.primaryForeground, textStyle: mutedTextStyle, + focusedBorderColor: colorScheme.foreground, radius: const Radius.circular(4), ), unselectedStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: mutedTextStyle, - focusedBackgroundColor: colorScheme.primaryForeground, + hoveredBackgroundColor: colorScheme.primaryForeground, + focusedBorderColor: colorScheme.foreground, radius: const Radius.circular(4), ), ), ), - disabledStyles: (current: disabled, enclosing: disabled), + unselectableStyles: (current: disabled, enclosing: disabled), ); } @@ -261,35 +267,35 @@ final class FCalendarDayPickerStyle with Diagnosticable { /// ```dart /// final style = FMonthStyle( /// headerTextStyle: ..., - /// enabledCurrent: ..., + /// selectableCurrent: ..., /// // Other arguments omitted for brevity. /// ); /// /// final copy = style.copyWith( - /// enabledCurrent: ..., + /// selectableCurrent: ..., /// ); /// /// print(style.headerTextStyle == copy.headerTextStyle); // true - /// print(style.enabled.current == copy.enabled.current); // false + /// print(style.selectableStyles.current == copy.selectableStyles.current); // false /// ``` @useResult FCalendarDayPickerStyle copyWith({ TextStyle? headerTextStyle, - FCalendarDayStyle? enabledCurrent, - FCalendarDayStyle? enabledEnclosing, - FCalendarDayStyle? disabledCurrent, - FCalendarDayStyle? disabledEnclosing, + FCalendarDayStyle? selectableCurrent, + FCalendarDayStyle? selectableEnclosing, + FCalendarDayStyle? unselectableCurrent, + FCalendarDayStyle? unselectableEnclosing, int? startDayOfWeek, }) => FCalendarDayPickerStyle( headerTextStyle: headerTextStyle ?? this.headerTextStyle, - enabledStyles: ( - current: enabledCurrent ?? enabledStyles.current, - enclosing: enabledEnclosing ?? enabledStyles.enclosing, + selectableStyles: ( + current: selectableCurrent ?? selectableStyles.current, + enclosing: selectableEnclosing ?? selectableStyles.enclosing, ), - disabledStyles: ( - current: disabledCurrent ?? disabledStyles.current, - enclosing: disabledEnclosing ?? disabledStyles.enclosing, + unselectableStyles: ( + current: unselectableCurrent ?? unselectableStyles.current, + enclosing: unselectableEnclosing ?? unselectableStyles.enclosing, ), startDayOfWeek: startDayOfWeek ?? this.startDayOfWeek, ); @@ -299,10 +305,10 @@ final class FCalendarDayPickerStyle with Diagnosticable { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('headerTextStyle', headerTextStyle)) - ..add(DiagnosticsProperty('enabled.current', enabledStyles.current)) - ..add(DiagnosticsProperty('enabled.enclosing', enabledStyles.enclosing)) - ..add(DiagnosticsProperty('disabled.current', disabledStyles.current)) - ..add(DiagnosticsProperty('disabled.enclosing', disabledStyles.enclosing)) + ..add(DiagnosticsProperty('selectableStyles.current', selectableStyles.current)) + ..add(DiagnosticsProperty('selectableStyles.enclosing', selectableStyles.enclosing)) + ..add(DiagnosticsProperty('unselectableStyles.current', unselectableStyles.current)) + ..add(DiagnosticsProperty('unselectableStyles.enclosing', unselectableStyles.enclosing)) ..add(IntProperty('startDayOfWeek', startDayOfWeek)); } @@ -312,13 +318,13 @@ final class FCalendarDayPickerStyle with Diagnosticable { other is FCalendarDayPickerStyle && runtimeType == other.runtimeType && headerTextStyle == other.headerTextStyle && - enabledStyles == other.enabledStyles && - disabledStyles == other.disabledStyles && + selectableStyles == other.selectableStyles && + unselectableStyles == other.unselectableStyles && startDayOfWeek == other.startDayOfWeek; @override int get hashCode => - headerTextStyle.hashCode ^ enabledStyles.hashCode ^ disabledStyles.hashCode ^ startDayOfWeek.hashCode; + headerTextStyle.hashCode ^ selectableStyles.hashCode ^ unselectableStyles.hashCode ^ startDayOfWeek.hashCode; } /// A calender day's style. diff --git a/forui/lib/src/widgets/calendar/day/paged_day_picker.dart b/forui/lib/src/widgets/calendar/day/paged_day_picker.dart index f98ffe294..d855c2954 100644 --- a/forui/lib/src/widgets/calendar/day/paged_day_picker.dart +++ b/forui/lib/src/widgets/calendar/day/paged_day_picker.dart @@ -25,7 +25,7 @@ class PagedDayPicker extends PagedPicker { required super.end, required super.today, required super.initial, - required super.enabled, + required super.selectable, super.key, }); @@ -36,7 +36,7 @@ class PagedDayPicker extends PagedPicker { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - ..add(DiagnosticsProperty('selectedPredicate', selected)) + ..add(DiagnosticsProperty('selected', selected)) ..add(DiagnosticsProperty('onMonthChange', onMonthChange)) ..add(DiagnosticsProperty('onPress', onPress)) ..add(DiagnosticsProperty('onLongPress', onLongPress)); @@ -50,7 +50,7 @@ class _PagedDayPickerState extends PagedPickerState { month: widget.start.truncate(to: DateUnit.months).plus(months: page), today: widget.today, focused: focusedDate, - enabled: widget.enabled, + selectable: widget.selectable, selected: widget.selected, onPress: (date) { setState(() => focusedDate = date); @@ -102,14 +102,14 @@ class _PagedDayPickerState extends PagedPickerState { // Can we use the preferred day in this month? if (preferredDay <= month.daysInMonth) { final newFocus = month.copyWith(day: preferredDay); - if (widget.enabled(newFocus)) { + if (widget.selectable(newFocus)) { return newFocus; } } // Start at the 1st and take the first enabled date. for (var newFocus = month; newFocus.month == month.month; newFocus = newFocus.tomorrow) { - if (widget.enabled(newFocus)) { + if (widget.selectable(newFocus)) { return newFocus; } } diff --git a/forui/lib/src/widgets/calendar/month/month_picker.dart b/forui/lib/src/widgets/calendar/month/month_picker.dart index 2d4db053a..7ab820ef4 100644 --- a/forui/lib/src/widgets/calendar/month/month_picker.dart +++ b/forui/lib/src/widgets/calendar/month/month_picker.dart @@ -5,7 +5,9 @@ import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; import 'package:sugar/sugar.dart'; +import 'package:forui/src/widgets/calendar/day/day_picker.dart'; import 'package:forui/src/widgets/calendar/shared/entry.dart'; +import 'package:forui/src/widgets/calendar/year/year_picker.dart'; import 'package:forui/src/widgets/calendar/year_month_picker.dart'; // ignore: non_constant_identifier_names @@ -64,23 +66,27 @@ class _MonthPickerState extends State { } @override - Widget build(BuildContext context) => GridView( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: MonthPicker.columns, - childAspectRatio: 1.618, + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(top: 5.0), + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: YearPicker.columns, + mainAxisExtent: ((DayPicker.tileDimension - 5.0) * DayPicker.maxRows) / YearPicker.rows, + mainAxisSpacing: 5.0, + ), + children: [ + for (var month = widget.currentYear, i = 0; i < 12; month = month.plus(months: 1), i++) + Entry.yearMonth( + style: widget.style, + date: month, + focusNode: _months[i], + current: widget.today.truncate(to: DateUnit.months) == month, + selectable: widget.start <= month && month <= widget.end, + format: (date) => _MMM.format(date.toNative()), // TODO: localize + onPress: widget.onPress, + ), + ], ), - children: [ - for (var month = widget.currentYear, i = 0; i < 12; month = month.plus(months: 1), i++) - Entry.yearMonth( - style: widget.style, - date: month, - focusNode: _months[i], - current: widget.today.truncate(to: DateUnit.months) == month, - enabled: widget.start <= month && month <= widget.end, - format: (date) => _MMM.format(date.toNative()), // TODO: localize - onPress: widget.onPress, - ), - ], ); @override diff --git a/forui/lib/src/widgets/calendar/month/paged_month_picker.dart b/forui/lib/src/widgets/calendar/month/paged_month_picker.dart index a59e9afc9..5294b9a7b 100644 --- a/forui/lib/src/widgets/calendar/month/paged_month_picker.dart +++ b/forui/lib/src/widgets/calendar/month/paged_month_picker.dart @@ -63,7 +63,7 @@ class _PagedMonthPickerState extends PagedPickerState { } for (var newFocus = widget.initial; newFocus < end; newFocus = newFocus.plus(months: 1)) { - if (widget.enabled(newFocus)) { + if (widget.selectable(newFocus)) { return newFocus; } } diff --git a/forui/lib/src/widgets/calendar/shared/entry.dart b/forui/lib/src/widgets/calendar/shared/entry.dart index 92882cdea..2dadeaf18 100644 --- a/forui/lib/src/widgets/calendar/shared/entry.dart +++ b/forui/lib/src/widgets/calendar/shared/entry.dart @@ -5,16 +5,15 @@ import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; import 'package:sugar/sugar.dart'; +import 'package:forui/forui.dart'; import 'package:forui/src/foundation/tappable.dart'; -import 'package:forui/src/widgets/calendar/day/day_picker.dart'; -import 'package:forui/src/widgets/calendar/year_month_picker.dart'; final _yMMMMd = DateFormat.yMMMMd(); @internal abstract class Entry extends StatelessWidget { final FCalendarEntryStyle style; - final ValueWidgetBuilder builder; + final ValueWidgetBuilder builder; factory Entry.day({ required FCalendarDayPickerStyle style, @@ -22,43 +21,42 @@ abstract class Entry extends StatelessWidget { required FocusNode focusNode, required bool current, required bool today, - required Predicate enabled, + required Predicate selectable, required Predicate selected, required ValueChanged onPress, required ValueChanged onLongPress, }) { - final enable = enabled(date); - final select = selected(date); + final canSelect = selectable(date); + final isSelected = selected(date); - final styles = enable ? style.enabledStyles : style.disabledStyles; + final styles = canSelect ? style.selectableStyles : style.unselectableStyles; final dayStyle = current ? styles.current : styles.enclosing; - final entryStyle = select ? dayStyle.selectedStyle : dayStyle.unselectedStyle; + final entryStyle = isSelected ? dayStyle.selectedStyle : dayStyle.unselectedStyle; - // ignore: avoid_positional_boolean_parameters - Widget builder(BuildContext context, bool focused, Widget? child) => _Content( + Widget builder(BuildContext context, FTappableState state, Widget? child) => _Content( style: entryStyle, borderRadius: BorderRadius.horizontal( left: selected(date.yesterday) ? Radius.zero : entryStyle.radius, right: selected(date.tomorrow) ? Radius.zero : entryStyle.radius, ), text: '${date.day}', // TODO: localization - focused: focused, + state: state, current: today, ); - if (enabled(date)) { - return _EnabledEntry( + if (canSelect) { + return _SelectableEntry( focusNode: focusNode, date: date, semanticLabel: '${_yMMMMd.format(date.toNative())}${today ? ', Today' : ''}', - selected: selected(date), + selected: isSelected, onPress: onPress, onLongPress: onLongPress, style: entryStyle, builder: builder, ); } else { - return _DisabledEntry(style: entryStyle, builder: builder); + return _UnselectableEntry(style: entryStyle, builder: builder); } } @@ -67,23 +65,22 @@ abstract class Entry extends StatelessWidget { required LocalDate date, required FocusNode focusNode, required bool current, - required bool enabled, + required bool selectable, required ValueChanged onPress, required String Function(LocalDate) format, }) { - final entryStyle = enabled ? style.enabledStyle : style.disabledStyle; + final entryStyle = selectable ? style.enabledStyle : style.disabledStyle; - // ignore: avoid_positional_boolean_parameters - Widget builder(BuildContext context, bool focused, Widget? child) => _Content( + Widget builder(BuildContext context, FTappableState state, Widget? child) => _Content( style: entryStyle, borderRadius: BorderRadius.all(entryStyle.radius), text: format(date), - focused: focused, + state: state, current: current, ); - if (enabled) { - return _EnabledEntry( + if (selectable) { + return _SelectableEntry( focusNode: focusNode, date: date, semanticLabel: format(date), @@ -92,7 +89,7 @@ abstract class Entry extends StatelessWidget { builder: builder, ); } else { - return _DisabledEntry(style: entryStyle, builder: builder); + return _UnselectableEntry(style: entryStyle, builder: builder); } } @@ -110,7 +107,7 @@ abstract class Entry extends StatelessWidget { } } -class _EnabledEntry extends Entry { +class _SelectableEntry extends Entry { final FocusNode focusNode; final LocalDate date; final String semanticLabel; @@ -118,7 +115,7 @@ class _EnabledEntry extends Entry { final ValueChanged onPress; final ValueChanged? onLongPress; - const _EnabledEntry({ + const _SelectableEntry({ required this.focusNode, required this.date, required this.semanticLabel, @@ -153,42 +150,44 @@ class _EnabledEntry extends Entry { } } -class _DisabledEntry extends Entry { - const _DisabledEntry({ +class _UnselectableEntry extends Entry { + const _UnselectableEntry({ required super.style, required super.builder, }) : super._(); @override - Widget build(BuildContext context) => ExcludeSemantics(child: builder(context, false, null)); + Widget build(BuildContext context) => + ExcludeSemantics(child: builder(context, (focused: false, hovered: false), null)); } class _Content extends StatelessWidget { final FCalendarEntryStyle style; final BorderRadius borderRadius; final String text; - final bool focused; + final FTappableState state; final bool current; const _Content({ required this.style, required this.borderRadius, required this.text, - required this.focused, + required this.state, required this.current, }); @override Widget build(BuildContext context) { - var textStyle = focused ? style.focusedTextStyle : style.textStyle; + var textStyle = state.hovered ? style.hoveredTextStyle : style.textStyle; if (current) { textStyle = textStyle.copyWith(decoration: TextDecoration.underline); } return DecoratedBox( decoration: BoxDecoration( + border: state.focused ? Border.all(color: context.theme.colorScheme.foreground) : null, borderRadius: borderRadius, - color: focused ? style.focusedBackgroundColor : style.backgroundColor, + color: state.hovered ? style.hoveredBackgroundColor : style.backgroundColor, ), child: Center( child: Text(text, style: textStyle), @@ -203,24 +202,27 @@ class _Content extends StatelessWidget { ..add(DiagnosticsProperty('style', style)) ..add(DiagnosticsProperty('borderRadius', borderRadius)) ..add(StringProperty('text', text)) - ..add(FlagProperty('focused', value: focused, ifTrue: 'focused')) + ..add(DiagnosticsProperty('state', state.toString())) ..add(FlagProperty('current', value: current, ifTrue: 'current')); } } /// A calendar entry's style. final class FCalendarEntryStyle with Diagnosticable { - /// The unfocused day's background color. + /// The day's background color. final Color backgroundColor; - /// The unfocused day's text style. + /// The day's text style. final TextStyle textStyle; - /// The focused day's background color. Defaults to [backgroundColor]. - final Color focusedBackgroundColor; + /// The hovered day's background color. Defaults to [backgroundColor]. + final Color hoveredBackgroundColor; - /// The focused day's text style. Defaults to [textStyle]. - final TextStyle focusedTextStyle; + /// The hovered day's text style. Defaults to [textStyle]. + final TextStyle hoveredTextStyle; + + /// The border color when an entry is focused. + final Color focusedBorderColor; /// The entry border's radius. Defaults to `Radius.circular(4)`. final Radius radius; @@ -229,11 +231,12 @@ final class FCalendarEntryStyle with Diagnosticable { FCalendarEntryStyle({ required this.backgroundColor, required this.textStyle, + required this.focusedBorderColor, required this.radius, - Color? focusedBackgroundColor, - TextStyle? focusedTextStyle, - }) : focusedBackgroundColor = focusedBackgroundColor ?? backgroundColor, - focusedTextStyle = focusedTextStyle ?? textStyle; + Color? hoveredBackgroundColor, + TextStyle? hoveredTextStyle, + }) : hoveredBackgroundColor = hoveredBackgroundColor ?? backgroundColor, + hoveredTextStyle = hoveredTextStyle ?? textStyle; /// Returns a copy of this [FCalendarEntryStyle] but with the given fields replaced with the new values. /// @@ -254,15 +257,17 @@ final class FCalendarEntryStyle with Diagnosticable { FCalendarEntryStyle copyWith({ Color? backgroundColor, TextStyle? textStyle, - Color? focusedBackgroundColor, - TextStyle? focusedTextStyle, + Color? hoveredBackgroundColor, + TextStyle? hoveredTextStyle, + Color? focusedBorderColor, Radius? radius, }) => FCalendarEntryStyle( backgroundColor: backgroundColor ?? this.backgroundColor, textStyle: textStyle ?? this.textStyle, - focusedBackgroundColor: focusedBackgroundColor ?? this.focusedBackgroundColor, - focusedTextStyle: focusedTextStyle ?? this.focusedTextStyle, + hoveredBackgroundColor: hoveredBackgroundColor ?? this.hoveredBackgroundColor, + hoveredTextStyle: hoveredTextStyle ?? this.hoveredTextStyle, + focusedBorderColor: focusedBorderColor ?? this.focusedBorderColor, radius: radius ?? this.radius, ); @@ -272,8 +277,9 @@ final class FCalendarEntryStyle with Diagnosticable { properties ..add(ColorProperty('backgroundColor', backgroundColor)) ..add(DiagnosticsProperty('textStyle', textStyle)) - ..add(ColorProperty('focusedBackgroundColor', focusedBackgroundColor)) - ..add(DiagnosticsProperty('focusedTextStyle', focusedTextStyle)) + ..add(ColorProperty('hoveredBackgroundColor', hoveredBackgroundColor)) + ..add(DiagnosticsProperty('hoveredTextStyle', hoveredTextStyle)) + ..add(ColorProperty('focusedBorderColor', focusedBorderColor)) ..add(DiagnosticsProperty('radius', radius)); } @@ -284,15 +290,17 @@ final class FCalendarEntryStyle with Diagnosticable { runtimeType == other.runtimeType && backgroundColor == other.backgroundColor && textStyle == other.textStyle && - focusedBackgroundColor == other.focusedBackgroundColor && - focusedTextStyle == other.focusedTextStyle && + hoveredBackgroundColor == other.hoveredBackgroundColor && + hoveredTextStyle == other.hoveredTextStyle && + focusedBorderColor == other.focusedBorderColor && radius == other.radius; @override int get hashCode => backgroundColor.hashCode ^ textStyle.hashCode ^ - focusedBackgroundColor.hashCode ^ - focusedTextStyle.hashCode ^ + hoveredBackgroundColor.hashCode ^ + hoveredTextStyle.hashCode ^ + focusedBorderColor.hashCode ^ radius.hashCode; } diff --git a/forui/lib/src/widgets/calendar/shared/header.dart b/forui/lib/src/widgets/calendar/shared/header.dart index de1c8b424..6d8ba6918 100644 --- a/forui/lib/src/widgets/calendar/shared/header.dart +++ b/forui/lib/src/widgets/calendar/shared/header.dart @@ -68,6 +68,7 @@ class _HeaderState extends State
with SingleTickerProviderStateMixin { }, excludeSemantics: true, child: Row( + mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_yMMMM.format(widget.month.toNative()), style: widget.style.headerTextStyle), // TODO: Localization diff --git a/forui/lib/src/widgets/calendar/shared/paged_picker.dart b/forui/lib/src/widgets/calendar/shared/paged_picker.dart index b991a9715..a092f77f8 100644 --- a/forui/lib/src/widgets/calendar/shared/paged_picker.dart +++ b/forui/lib/src/widgets/calendar/shared/paged_picker.dart @@ -15,7 +15,7 @@ abstract class PagedPicker extends StatefulWidget { final LocalDate end; final LocalDate today; final LocalDate initial; - final Predicate enabled; + final Predicate selectable; PagedPicker({ required this.style, @@ -23,9 +23,9 @@ abstract class PagedPicker extends StatefulWidget { required this.end, required this.today, required this.initial, - Predicate? enabled, + Predicate? selectable, super.key, - }) : enabled = ((date) => start <= date && date <= end && (enabled?.call(date) ?? true)); + }) : selectable = ((date) => start <= date && date <= end && (selectable?.call(date) ?? true)); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -36,7 +36,7 @@ abstract class PagedPicker extends StatefulWidget { ..add(DiagnosticsProperty('end', end)) ..add(DiagnosticsProperty('today', today)) ..add(DiagnosticsProperty('initial', initial)) - ..add(DiagnosticsProperty('enabledPredicate', enabled)); + ..add(DiagnosticsProperty('selectable', selectable)); } } @@ -197,7 +197,7 @@ abstract class PagedPickerState extends State { var next = date + offset; while (widget.start <= next && next <= widget.end) { - if (widget.enabled(next)) { + if (widget.selectable(next)) { return next; } diff --git a/forui/lib/src/widgets/calendar/year/paged_year_picker.dart b/forui/lib/src/widgets/calendar/year/paged_year_picker.dart index ceabfe331..0b9391627 100644 --- a/forui/lib/src/widgets/calendar/year/paged_year_picker.dart +++ b/forui/lib/src/widgets/calendar/year/paged_year_picker.dart @@ -79,7 +79,7 @@ class _PagedYearPickerState extends PagedPickerState { } for (var newFocus = startYear; newFocus < endYear; newFocus = newFocus.plus(years: 1)) { - if (widget.enabled(newFocus)) { + if (widget.selectable(newFocus)) { return newFocus; } } diff --git a/forui/lib/src/widgets/calendar/year/year_picker.dart b/forui/lib/src/widgets/calendar/year/year_picker.dart index 492fb9877..a0d3efc24 100644 --- a/forui/lib/src/widgets/calendar/year/year_picker.dart +++ b/forui/lib/src/widgets/calendar/year/year_picker.dart @@ -4,13 +4,14 @@ import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; import 'package:sugar/sugar.dart'; +import 'package:forui/src/widgets/calendar/day/day_picker.dart'; import 'package:forui/src/widgets/calendar/shared/entry.dart'; import 'package:forui/src/widgets/calendar/year_month_picker.dart'; @internal class YearPicker extends StatefulWidget { static const columns = 3; - static const rows = 4; + static const rows = 5; static const items = columns * rows; final FCalendarYearMonthPickerStyle style; @@ -66,23 +67,27 @@ class _YearPickerState extends State { } @override - Widget build(BuildContext context) => GridView( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: YearPicker.columns, - childAspectRatio: 1.618, + Widget build(BuildContext context) => Padding( + padding: const EdgeInsets.only(top: 5.0), + child: GridView( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: YearPicker.columns, + mainAxisExtent: ((DayPicker.tileDimension - 5.0) * DayPicker.maxRows) / YearPicker.rows, + mainAxisSpacing: 5.0, + ), + children: [ + for (var year = widget.startYear, i = 0; i < YearPicker.items; year = year.plus(years: 1), i++) + Entry.yearMonth( + style: widget.style, + date: year, + focusNode: _years[i], + current: widget.today.year == year.year, + selectable: widget.start <= year && year <= widget.end, + format: (date) => '${date.year}', // TODO: localization + onPress: widget.onPress, + ), + ], ), - children: [ - for (var year = widget.startYear, i = 0; i < YearPicker.items; year = year.plus(years: 1), i++) - Entry.yearMonth( - style: widget.style, - date: year, - focusNode: _years[i], - current: widget.today.year == year.year, - enabled: widget.start <= year && year <= widget.end, - format: (date) => '${date.year}', // TODO: localization - onPress: widget.onPress, - ), - ], ); @override diff --git a/forui/lib/src/widgets/calendar/year_month_picker.dart b/forui/lib/src/widgets/calendar/year_month_picker.dart index b28fc9942..f7152bec8 100644 --- a/forui/lib/src/widgets/calendar/year_month_picker.dart +++ b/forui/lib/src/widgets/calendar/year_month_picker.dart @@ -92,13 +92,15 @@ final class FCalendarYearMonthPickerStyle with Diagnosticable { enabledStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: typography.sm.copyWith(color: colorScheme.foreground, fontWeight: FontWeight.w500), - focusedBackgroundColor: colorScheme.secondary, + hoveredBackgroundColor: colorScheme.secondary, + focusedBorderColor: colorScheme.foreground, radius: const Radius.circular(8), ), disabledStyle: FCalendarEntryStyle( backgroundColor: colorScheme.background, textStyle: typography.sm .copyWith(color: colorScheme.mutedForeground.withOpacity(0.5), fontWeight: FontWeight.w500), + focusedBorderColor: colorScheme.background, radius: const Radius.circular(8), ), ); diff --git a/forui/test/golden/calendar/month-picker/zinc-dark-default.png b/forui/test/golden/calendar/month-picker/zinc-dark-default.png index ea656d363..c23afc0ea 100644 Binary files a/forui/test/golden/calendar/month-picker/zinc-dark-default.png 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 index 01504167d..6486861bc 100644 Binary files a/forui/test/golden/calendar/month-picker/zinc-light-default.png 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 index 755b2533a..7a81d687b 100644 Binary files a/forui/test/golden/calendar/year-picker/zinc-dark-default.png 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 index a2dc2edf0..30e8cdae3 100644 Binary files a/forui/test/golden/calendar/year-picker/zinc-dark-initial-date.png 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 index 78a0f9a03..568345382 100644 Binary files a/forui/test/golden/calendar/year-picker/zinc-light-default.png 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 index cfb7ae928..1c5d6103e 100644 Binary files a/forui/test/golden/calendar/year-picker/zinc-light-initial-date.png and b/forui/test/golden/calendar/year-picker/zinc-light-initial-date.png differ diff --git a/forui/test/src/foundation/tappable_test.dart b/forui/test/src/foundation/tappable_test.dart index 85898d9db..0d106e2b9 100644 --- a/forui/test/src/foundation/tappable_test.dart +++ b/forui/test/src/foundation/tappable_test.dart @@ -21,11 +21,11 @@ void main() { ), ), ); - expect(find.text('false'), findsOneWidget); + expect(find.text((focused: false, hovered: false).toString()), findsOneWidget); focusNode.requestFocus(); await tester.pumpAndSettle(); - expect(find.text('true'), findsOneWidget); + expect(find.text((focused: true, hovered: false).toString()), findsOneWidget); }); testWidgets('hovered', (tester) async { @@ -37,7 +37,7 @@ void main() { ), ), ); - expect(find.text('false'), findsOneWidget); + expect(find.text((focused: false, hovered: false).toString()), findsOneWidget); final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); await gesture.addPointer(location: Offset.zero); @@ -47,12 +47,12 @@ void main() { await gesture.moveTo(tester.getCenter(find.byType(FTappable))); await tester.pumpAndSettle(); - expect(find.text('true'), findsOneWidget); + expect(find.text((focused: false, hovered: true).toString()), findsOneWidget); await gesture.moveTo(Offset.zero); await tester.pumpAndSettle(); - expect(find.text('false'), findsOneWidget); + expect(find.text((focused: false, hovered: false).toString()), findsOneWidget); }); }); } diff --git a/forui/test/src/widgets/calendar/calendar_controller_test.dart b/forui/test/src/widgets/calendar/calendar_controller_test.dart index 820f8bc7a..3d59ecb35 100644 --- a/forui/test/src/widgets/calendar/calendar_controller_test.dart +++ b/forui/test/src/widgets/calendar/calendar_controller_test.dart @@ -3,10 +3,10 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:forui/forui.dart'; void main() { - group('FCalendarSingleValueController', () { + group('FCalendarValueController', () { test( 'constructor throws error', - () => expect(() => FCalendarSingleValueController(DateTime.now()), throwsAssertionError), + () => expect(() => FCalendarValueController(initialSelection: DateTime.now()), throwsAssertionError), ); for (final (date, expected) in [ @@ -14,8 +14,8 @@ void main() { (DateTime.utc(2024, 5, 5), false), ]) { test('contains(...) contains date', () { - final controller = FCalendarSingleValueController(DateTime.utc(2024, 5, 4)); - expect(controller.contains(date), expected); + final controller = FCalendarValueController(initialSelection: DateTime.utc(2024, 5, 4)); + expect(controller.selected(date), expected); }); } @@ -25,8 +25,8 @@ void main() { (DateTime.utc(2024), DateTime.utc(2025), DateTime.utc(2025)), (DateTime.utc(2024), DateTime.utc(2024), null), ]) { - test('onPress(...)', () { - final controller = FCalendarSingleValueController(initial)..onPress(date); + test('select(...)', () { + final controller = FCalendarValueController(initialSelection: initial)..select(date); expect(controller.value, expected); }); } @@ -38,8 +38,8 @@ void main() { (DateTime.utc(2025), false), ]) { test('contains(...)', () { - final controller = FCalendarMultiValueController({DateTime.utc(2024)}); - expect(controller.contains(date), expected); + final controller = FCalendarMultiValueController(initialSelections: {DateTime.utc(2024)}); + expect(controller.selected(date), expected); }); } @@ -48,17 +48,20 @@ void main() { ({}, 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); + test('select(...)', () { + final controller = FCalendarMultiValueController(initialSelections: initial)..select(date); expect(controller.value, expected); }); } }); - group('FCalendarSingleRangeController', () { + group('FCalendarRangeController', () { test( 'constructor throws error', - () => expect(() => FCalendarSingleRangeController((DateTime(2025), DateTime(2024))), throwsAssertionError), + () => expect( + () => FCalendarRangeController(initialSelection: (DateTime(2025), DateTime(2024))), + throwsAssertionError, + ), ); for (final (initial, date, expected) in [ @@ -68,9 +71,9 @@ void main() { ((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); + test('selected(...)', () { + final controller = FCalendarRangeController(initialSelection: initial); + expect(controller.selected(date), expected); }); } @@ -82,8 +85,8 @@ void main() { ((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); + test('select(...)', () { + final controller = FCalendarRangeController(initialSelection: initial)..select(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 index dc78db1f3..61a07af59 100644 --- a/forui/test/src/widgets/calendar/calendar_golden_test.dart +++ b/forui/test/src/widgets/calendar/calendar_golden_test.dart @@ -29,8 +29,10 @@ void main() { child: Padding( padding: const EdgeInsets.all(16), child: FCalendar( - controller: FCalendarMultiValueController(selected), - enabled: (date) => date != DateTime.utc(2024, 7, 2), + controller: FCalendarMultiValueController( + initialSelections: selected, + canSelect: (date) => date != DateTime.utc(2024, 7, 2), + ), start: DateTime(1900, 1, 8), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), @@ -61,7 +63,7 @@ void main() { child: Padding( padding: const EdgeInsets.all(16), child: FCalendar( - controller: FCalendarMultiValueController(selected), + controller: FCalendarMultiValueController(initialSelections: selected), start: DateTime(1900, 1, 8), end: DateTime(2024, 7, 10), today: DateTime(2024, 6, 14), @@ -86,7 +88,7 @@ void main() { child: Padding( padding: const EdgeInsets.all(16), child: FCalendar( - controller: FCalendarMultiValueController(selected), + controller: FCalendarMultiValueController(initialSelections: selected), start: DateTime(1900, 1, 8), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), @@ -123,7 +125,7 @@ void main() { child: Padding( padding: const EdgeInsets.all(16), child: FCalendar( - controller: FCalendarMultiValueController(selected), + controller: FCalendarMultiValueController(initialSelections: selected), start: DateTime(1900, 1, 8), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), @@ -147,11 +149,11 @@ void main() { child: Padding( padding: const EdgeInsets.all(16), child: FCalendar( - controller: FCalendarMultiValueController(selected), + controller: FCalendarMultiValueController(initialSelections: selected), start: DateTime(1900, 1, 8), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), - initialDate: DateTime(1984, 4, 2), + initialMonth: DateTime(1984, 4, 2), initialType: FCalendarPickerType.yearMonth, ), ), @@ -163,7 +165,7 @@ void main() { addTearDown(gesture.removePointer); await tester.pump(); - await gesture.moveTo(tester.getCenter(find.text('1991'))); + await gesture.moveTo(tester.getCenter(find.text('1989'))); await tester.pumpAndSettle(); await expectLater( diff --git a/forui/test/src/widgets/calendar/calendar_test.dart b/forui/test/src/widgets/calendar/calendar_test.dart index 386d31e6c..5221269c9 100644 --- a/forui/test/src/widgets/calendar/calendar_test.dart +++ b/forui/test/src/widgets/calendar/calendar_test.dart @@ -11,8 +11,7 @@ void main() { TestScaffold( data: FThemes.zinc.light, child: FCalendar( - controller: FCalendarMultiValueController(), - enabled: (date) => date != DateTime.utc(2024, 7, 2), + controller: FCalendarMultiValueController(canSelect: (date) => date != DateTime.utc(2024, 7, 2)), start: DateTime(1900, 1, 8), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), @@ -33,8 +32,7 @@ void main() { TestScaffold( data: FThemes.zinc.light, child: FCalendar( - controller: FCalendarMultiValueController(), - enabled: (date) => date != DateTime.utc(2024, 7, 2), + controller: FCalendarMultiValueController(canSelect: (date) => date != DateTime.utc(2024, 7, 2)), start: DateTime(2024, 7), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), @@ -57,8 +55,7 @@ void main() { TestScaffold( data: FThemes.zinc.light, child: FCalendar( - controller: FCalendarMultiValueController(), - enabled: (date) => date != DateTime.utc(2024, 7, 2), + controller: FCalendarMultiValueController(canSelect: (date) => date != DateTime.utc(2024, 7, 2)), start: DateTime(1900, 1, 8), end: DateTime(2024, 8, 10), today: DateTime(2024, 7, 14), @@ -79,8 +76,7 @@ void main() { TestScaffold( data: FThemes.zinc.light, child: FCalendar( - controller: FCalendarMultiValueController(), - enabled: (date) => date != DateTime.utc(2024, 7, 2), + controller: FCalendarMultiValueController(canSelect: (date) => date != DateTime.utc(2024, 7, 2)), start: DateTime(2024), end: DateTime(2024, 7, 10), today: DateTime(2024, 7, 14), diff --git a/samples/lib/main.dart b/samples/lib/main.dart index fc7c6e686..9743f7b18 100644 --- a/samples/lib/main.dart +++ b/samples/lib/main.dart @@ -72,8 +72,12 @@ class _AppRouter extends $_AppRouter { page: MultiValueCalendarRoute.page, ), AutoRoute( - path: '/calendar/single-range', - page: SingleRangeCalendarRoute.page, + path: '/calendar/unselectable', + page: UnselectableCalendarRoute.page, + ), + AutoRoute( + path: '/calendar/range', + page: RangeCalendarRoute.page, ), AutoRoute( path: '/card/default', diff --git a/samples/lib/widgets/calendar.dart b/samples/lib/widgets/calendar.dart index 24396ec09..80e352f37 100644 --- a/samples/lib/widgets/calendar.dart +++ b/samples/lib/widgets/calendar.dart @@ -24,7 +24,7 @@ class CalendarPage extends SampleScaffold { @override Widget child(BuildContext context) => FCalendar( - controller: FCalendarSingleValueController(selected), + controller: FCalendarValueController(initialSelection: selected), start: DateTime.utc(2000), end: DateTime.utc(2030), ); @@ -38,22 +38,46 @@ class MultiValueCalendarPage extends SampleScaffold { @override Widget child(BuildContext context) => FCalendar( - controller: FCalendarMultiValueController({selected}), + controller: FCalendarMultiValueController( + initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, + ), start: DateTime.utc(2000), + today: DateTime.utc(2024, 7, 15), end: DateTime.utc(2030), ); } @RoutePage() -class SingleRangeCalendarPage extends SampleScaffold { - SingleRangeCalendarPage({ +class UnselectableCalendarPage extends SampleScaffold { + UnselectableCalendarPage({ @queryParam super.theme, }); @override Widget child(BuildContext context) => FCalendar( - controller: FCalendarSingleRangeController((selected, selected)), + controller: FCalendarMultiValueController( + initialSelections: {DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)}, + canSelect: (date) => !{DateTime.utc(2024, 7, 18), DateTime.utc(2024, 7, 19)}.contains(date), + ), start: DateTime.utc(2000), + today: DateTime.utc(2024, 7, 15), + end: DateTime.utc(2030), + ); +} + +@RoutePage() +class RangeCalendarPage extends SampleScaffold { + RangeCalendarPage({ + @queryParam super.theme, + }); + + @override + Widget child(BuildContext context) => FCalendar( + controller: FCalendarRangeController( + initialSelection: (DateTime.utc(2024, 7, 17), DateTime.utc(2024, 7, 20)), + ), + start: DateTime.utc(2000), + today: DateTime.utc(2024, 7, 15), end: DateTime.utc(2030), ); }