Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Differentiate between focused and hovered states on FTappable & fix misc. calendar bugs #94

Merged
merged 15 commits into from
Jul 26, 2024
Merged
11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
56 changes: 43 additions & 13 deletions docs/pages/docs/calendar.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
Expand All @@ -14,7 +17,7 @@ to transition between pages.
<Tabs.Tab>
```dart
FCalendar(
controller: FCalendarSingleRangeController(),
controller: FCalendarValueController(initialSelection: selected),
start: DateTime.utc(2000),
end: DateTime.utc(2030),
);
Expand All @@ -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),
Expand All @@ -58,34 +63,59 @@ FCalendar(
</Tabs.Tab>
</Tabs>

### Multiple Dates
### Multiple Dates with Initial Selections
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' variant='multi-value' query={{}} height={500}/>
</Tabs.Tab>
<Tabs.Tab>
```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),
);
```
</Tabs.Tab>
</Tabs>

### Single Range
### Unselectable Dates
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' variant='unselectable' query={{}} height={500}/>
</Tabs.Tab>
```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),
);
```
</Tabs.Tab>
</Tabs>

### Range Selection with Initial Range
<Tabs items={['Preview', 'Code']}>
<Tabs.Tab>
<Widget name='calendar' variant='single-range' query={{}} height={500}/>
<Widget name='calendar' variant='range' query={{}} height={500}/>
</Tabs.Tab>
<Tabs.Tab>
```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),
)
);
```
</Tabs.Tab>
</Tabs>
26 changes: 26 additions & 0 deletions forui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@

### Additions
* Add `FAvatar`
* **Breaking:** Add `FCalendarEntryStyle.focusedBorderColor`. This only affects users that customized `FCalendarEntryStyle`.
* Add `FResizable`

### Changes
Pante marked this conversation as resolved.
Show resolved Hide resolved
* Change number of years displayed per page in `FCalendar` from 12 to 15.
* **Breaking:** Move `FCalendar.enabled` to `FCalendarController.canSelect(...)`.
Pante marked this conversation as resolved.
Show resolved Hide resolved

* **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

Expand All @@ -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
Expand All @@ -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! 🚀
13 changes: 12 additions & 1 deletion forui/example/lib/example.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';

import 'package:forui/forui.dart';

class Example extends StatefulWidget {
const Example({super.key});

Expand All @@ -14,5 +16,14 @@ class _ExampleState extends State<Example> {
}

@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),
),
],
);
}
2 changes: 1 addition & 1 deletion forui/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class _ApplicationState extends State<Application> {
),
],
),
content: const Example(),
content: child!,
footer: FBottomNavigationBar(
index: index,
onChange: (index) => setState(() => this.index = index),
Expand Down
82 changes: 53 additions & 29 deletions forui/lib/src/foundation/tappable.dart
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,7 +19,7 @@ class FTappable extends StatefulWidget {
final ValueChanged<bool>? onFocusChange;
final VoidCallback? onPress;
final VoidCallback? onLongPress;
final ValueWidgetBuilder<bool> builder;
final ValueWidgetBuilder<FTappableState> builder;
final Widget? child;

factory FTappable.animated({
Expand All @@ -26,7 +31,7 @@ class FTappable extends StatefulWidget {
ValueChanged<bool>? onFocusChange,
VoidCallback? onPress,
VoidCallback? onLongPress,
ValueWidgetBuilder<bool>? builder,
ValueWidgetBuilder<FTappableState>? builder,
Widget? child,
Key? key,
}) = _AnimatedTappable;
Expand All @@ -40,12 +45,13 @@ class FTappable extends StatefulWidget {
this.onFocusChange,
this.onPress,
this.onLongPress,
ValueWidgetBuilder<bool>? builder,
ValueWidgetBuilder<FTappableState>? 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<FTappable> createState() => _FTappableState();
Expand Down Expand Up @@ -80,33 +86,51 @@ class _FTappableState extends State<FTappable> 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<ActivateIntent>(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),
);
}

Expand Down Expand Up @@ -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),
),
);

Expand Down
Loading