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),
);
}