Skip to content

Commit

Permalink
Add documentation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Pante committed Jul 14, 2024
1 parent 6827db0 commit a431156
Show file tree
Hide file tree
Showing 24 changed files with 568 additions and 60 deletions.
91 changes: 91 additions & 0 deletions docs/pages/docs/calendar.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Tabs } from 'nextra/components';
import { Widget } from "../../components/widget";

# Calendar
A date field component that allows users to enter and edit date.

The calendar pages are designed to be navigable through swipe gestures on mobile platforms, allowing left and right swipes
to transition between pages.

<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' query={{}} height={500}/>
</Tabs.Tab>
<Tabs.Tab>
```dart
FCalendar(
controller: FCalendarSingleRangeController(),
start: DateTime.utc(2024),
end: DateTime.utc(2030),
);
```
</Tabs.Tab>
</Tabs>

## Usage

### `FCalendar(...)`

```dart
FCalendar(
controller: FCalendarSingleRangeController(),
start: DateTime.utc(2024),
end: DateTime.utc(2030),
today: DateTime.utc(2024, 7, 14),
initalType = FCalendarPickerType.yearMonth,
initialDate = DateTime.utc(2024, 9, 12),
enabled: (date) => allowed.contains(date),
onMonthChange: (date) => print(date),
onPress: (date) => print(date),
onLongPress: (date) => print(date),
);
```
## Examples

### Single Date
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' query={{}} height={500}/>
</Tabs.Tab>
<Tabs.Tab>
```dart
FCalendar(
controller: FCalendarSingleValueController(),
start: DateTime.utc(2024),
end: DateTime.utc(2030),
);
```
</Tabs.Tab>
</Tabs>

### Multiple Dates
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' variant='multi-value' query={{}} height={500}/>
</Tabs.Tab>
<Tabs.Tab>
```dart
FCalendar(
controller: FCalendarMultiValueController(),
start: DateTime.utc(2024),
end: DateTime.utc(2030),
);
```
</Tabs.Tab>
</Tabs>

### Single Range
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' variant='single-range' query={{}} height={500}/>
</Tabs.Tab>
<Tabs.Tab>
```dart
FCalendar(
controller: FCalendarSingleRangeController(),
start: DateTime.utc(2024),
end: DateTime.utc(2030),
)
```
</Tabs.Tab>
</Tabs>
3 changes: 3 additions & 0 deletions forui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Next

### Additions
* Add `FCalendar`

### Enhancements
* **Breaking** Change `FSwitch` to be usable in `Form`s.
* **Breaking** Rename `FThemeData.checkBoxStyle` to `FThemeData.checkboxStyle` for consistency.
Expand Down
24 changes: 2 additions & 22 deletions forui/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';

import 'package:forui/forui.dart';
import 'package:forui_example/example.dart';


void main() {
Expand Down Expand Up @@ -29,27 +30,6 @@ class Application extends StatelessWidget {
content: child ?? const SizedBox(),
),
),
home: Column(
children: [
const Testing(),
// FHeaderAction(
// icon: FAssets.icons.plus,
// onPress: () => showDatePicker(context: context, firstDate: DateTime(2024, 7, 1), lastDate: DateTime(2024, 7, 31), initialDate: DateTime(2024, 7, 8)),
// ),
],
),
);
}

class Testing extends StatelessWidget {
static final _selected = {DateTime(2024, 7, 16), DateTime(2024, 7, 17), DateTime(2024, 7, 18), DateTime(2024, 7, 29)};

const Testing({super.key});

@override
Widget build(BuildContext context) => FCalendar(
start: DateTime(1900, 1, 8),
end: DateTime(2024, 7, 10),
controller: FCalendarMultiValueController(),
home: const Example(),
);
}
9 changes: 5 additions & 4 deletions forui/lib/src/widgets/calendar/calendar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export 'year_month_picker.dart' show FCalendarYearMonthPickerStyle;

/// A calendar.
///
/// The calendar pages are designed to be navigable through swipe gestures on mobile Android, iOS & iPadOS, allowing
/// left and right swipes to transition between pages.
///
/// See:
/// * https://forui.dev/docs/calendar for working examples.
/// * [FCalendarDayStyle] for customizing a card's appearance.
Expand Down Expand Up @@ -104,9 +107,7 @@ class FCalendar extends StatelessWidget {
ValueListenableBuilder(
valueListenable: _type,
builder: (context, value, child) => switch (value) {
FCalendarPickerType.day => ValueListenableBuilder(
valueListenable: controller,
builder: (context, _, __) => PagedDayPicker(
FCalendarPickerType.day => PagedDayPicker(
style: style,
start: start.toLocalDate(),
end: end.toLocalDate(),
Expand All @@ -125,12 +126,12 @@ class FCalendar extends StatelessWidget {
},
onLongPress: (date) => onLongPress?.call(date.toNative()),
),
),
FCalendarPickerType.yearMonth => YearMonthPicker(
style: style,
start: start.toLocalDate(),
end: end.toLocalDate(),
today: today.toLocalDate(),
initial: _month.value,
onChange: (date) {
_month.value = date;
_type.value = FCalendarPickerType.day;
Expand Down
30 changes: 18 additions & 12 deletions forui/lib/src/widgets/calendar/calendar_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import 'package:forui/forui.dart';
import 'package:sugar/sugar.dart';

/// A controller that controls date selection in a calendar.
///
/// This class should be extended to customize date selection. By default, the following controllers are provided:
/// * [FCalendarSingleValueController] for selecting a single date.
/// * [FCalendarMultiValueController] for selecting multiple date.
/// * [FCalendarSingleRangeController] for selecting a single range.
abstract class FCalendarController<T> extends ValueNotifier<T> {
/// Creates a [FCalendarController] with the given initial [value].
FCalendarController(super._value);
Expand Down Expand Up @@ -30,13 +35,7 @@ final class FCalendarSingleValueController extends FCalendarController<DateTime?
bool contains(DateTime date) => value?.toLocalDate() == date.toLocalDate();

@override
void onPress(DateTime date) {
if (value?.toLocalDate() == date.toLocalDate()) {
value = null;
} else {
value = date;
}
}
void onPress(DateTime date) => value = value?.toLocalDate() == date.toLocalDate() ? null : date;
}

/// A date selection controller that allows multiple dates to be selected.
Expand All @@ -47,14 +46,15 @@ final class FCalendarMultiValueController extends FCalendarController<Set<DateTi
///
/// ## Contract:
/// Throws an [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');
FCalendarMultiValueController([super.value = const {}])
: assert(value.every((d) => d.isUtc), 'dates must be in UTC timezone');

@override
bool contains(DateTime date) => value.contains(date);

@override
void onPress(DateTime date) {
final copy = { ...value };
final copy = {...value};
value = copy..toggle(date);
}
}
Expand All @@ -67,9 +67,15 @@ final class FCalendarSingleRangeController extends FCalendarController<(DateTime
/// Creates a [FCalendarSingleRangeController] with the given initial [value].
///
/// ## Contract:
/// Throws an [AssertionError] if the given [value] is not in UTC timezone.
/// Throws an [AssertionError] if:
/// * the given dates in [value] is not in UTC timezone.
/// * the end date is less than start date.
FCalendarSingleRangeController([super.value])
: assert(value == null || (value.$1.isUtc && value.$2.isUtc), 'value must be in UTC timezone');
: assert(value == null || (value.$1.isUtc && value.$2.isUtc), 'value must be in UTC timezone'),
assert(
value == null || (value.$1.isBefore(value.$2) || value.$1.isAtSameMomentAs(value.$2)),
'end date must be greater than or equal to start date',
);

@override
bool contains(DateTime date) {
Expand Down Expand Up @@ -98,7 +104,7 @@ final class FCalendarSingleRangeController extends FCalendarController<(DateTime
case (final first, final last) when pressed < first:
value = (pressed.toNative(), last.toNative());

case (final first, _):
case (final first, _):
value = (first.toNative(), pressed.toNative());
}
}
Expand Down
5 changes: 3 additions & 2 deletions forui/lib/src/widgets/calendar/month/month_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import 'package:intl/intl.dart';
import 'package:meta/meta.dart';
import 'package:sugar/sugar.dart';

final _yMMMM = DateFormat.yMMMM();
// ignore: non_constant_identifier_names
final _MMM = DateFormat.MMM();

@internal
class MonthPicker extends StatefulWidget {
Expand Down Expand Up @@ -74,7 +75,7 @@ class _MonthPickerState extends State<MonthPicker> {
focusNode: _months[i],
current: widget.today.truncate(to: DateUnit.months) == month,
enabled: widget.start <= month && month <= widget.end,
format: (date) => _yMMMM.format(date.toNative()), // TODO: localize
format: (date) => _MMM.format(date.toNative()), // TODO: localize
onPress: widget.onPress,
),
],
Expand Down
19 changes: 12 additions & 7 deletions forui/lib/src/widgets/calendar/shared/header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,18 @@ class _HeaderState extends State<Header> with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this, duration: widget.style.animationDuration);
widget.type.addListener(_animate);
_controller = AnimationController(vsync: this, duration: widget.style.animationDuration);
_controller.value = widget.type.value == FCalendarPickerType.day ? 0.0 : 1.0;
}

@override
Widget build(BuildContext context) => SizedBox(
height: Header.height,
child: FInkWell(
onPress: () => widget.type.value = switch (widget.type.value) {
FCalendarPickerType.day => FCalendarPickerType.day,
FCalendarPickerType.yearMonth => FCalendarPickerType.yearMonth,
FCalendarPickerType.day => FCalendarPickerType.yearMonth,
FCalendarPickerType.yearMonth => FCalendarPickerType.day,
},
builder: (context, _, child) => child!,
child: Row(
Expand Down Expand Up @@ -98,10 +99,14 @@ class _HeaderState extends State<Header> with SingleTickerProviderStateMixin {
}

void _animate() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
// we check the picker type to prevent de-syncs
switch ((widget.type.value, _controller.isCompleted)) {
case (FCalendarPickerType.yearMonth, false):
_controller.forward();
case (FCalendarPickerType.day, true):
_controller.reverse();

case _:
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion forui/lib/src/widgets/calendar/year_month_picker.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ class YearMonthPicker extends StatefulWidget {
final LocalDate start;
final LocalDate end;
final LocalDate today;
final LocalDate initial;
final ValueChanged<LocalDate> onChange;

const YearMonthPicker({
required this.style,
required this.start,
required this.end,
required this.today,
required this.initial,
required this.onChange,
super.key,
});
Expand All @@ -34,6 +36,7 @@ class YearMonthPicker extends StatefulWidget {
..add(DiagnosticsProperty('start', start))
..add(DiagnosticsProperty('end', end))
..add(DiagnosticsProperty('today', today))
..add(DiagnosticsProperty('initial', initial))
..add(DiagnosticsProperty('onChange', onChange));
}
}
Expand All @@ -54,7 +57,7 @@ class _YearMonthPickerState extends State<YearMonthPicker> {
start: widget.start,
end: widget.end,
today: widget.today,
initial: widget.today.truncate(to: DateUnit.years),
initial: widget.initial.truncate(to: DateUnit.years),
onPress: (year) => setState(() => _date = year),
);
} else {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a431156

Please sign in to comment.