fix: hide task quick actions until hover
This commit is contained in:
parent
dacd189435
commit
5c6649945d
2 changed files with 137 additions and 67 deletions
|
|
@ -11,7 +11,7 @@ import 'difficulty_bars.dart';
|
||||||
import 'reward_icon.dart';
|
import 'reward_icon.dart';
|
||||||
|
|
||||||
/// Interactive card that renders a scheduled task on the compact timeline.
|
/// Interactive card that renders a scheduled task on the compact timeline.
|
||||||
class TaskTimelineCard extends StatelessWidget {
|
class TaskTimelineCard extends StatefulWidget {
|
||||||
/// Creates a task timeline card.
|
/// Creates a task timeline card.
|
||||||
const TaskTimelineCard({required this.card, required this.onTap, super.key});
|
const TaskTimelineCard({required this.card, required this.onTap, super.key});
|
||||||
|
|
||||||
|
|
@ -21,8 +21,16 @@ class TaskTimelineCard extends StatelessWidget {
|
||||||
/// Callback invoked when the card is tapped.
|
/// Callback invoked when the card is tapped.
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TaskTimelineCard> createState() => _TaskTimelineCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TaskTimelineCardState extends State<TaskTimelineCard> {
|
||||||
|
bool _isHovered = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final card = widget.card;
|
||||||
final colors = _CardColors.forKind(card.visualKind);
|
final colors = _CardColors.forKind(card.visualKind);
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
|
@ -30,12 +38,23 @@ class TaskTimelineCard extends StatelessWidget {
|
||||||
constraints,
|
constraints,
|
||||||
kind: card.visualKind,
|
kind: card.visualKind,
|
||||||
);
|
);
|
||||||
return Material(
|
return MouseRegion(
|
||||||
|
onEnter: (_) {
|
||||||
|
setState(() {
|
||||||
|
_isHovered = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onExit: (_) {
|
||||||
|
setState(() {
|
||||||
|
_isHovered = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
key: ValueKey(card.id),
|
key: ValueKey(card.id),
|
||||||
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
borderRadius: BorderRadius.circular(metrics.cardRadius),
|
||||||
onTap: onTap,
|
onTap: widget.onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: metrics.padding,
|
padding: metrics.padding,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|
@ -68,29 +87,12 @@ class TaskTimelineCard extends StatelessWidget {
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (card.showsQuickActions) ...[
|
if (card.showsQuickActions)
|
||||||
_NoOpIcon(
|
_QuickActions(
|
||||||
icon: Icons.check,
|
|
||||||
color: colors.accent,
|
color: colors.accent,
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
|
visible: _isHovered,
|
||||||
),
|
),
|
||||||
_NoOpIcon(
|
|
||||||
icon: Icons.arrow_forward,
|
|
||||||
color: colors.accent,
|
|
||||||
metrics: metrics,
|
|
||||||
),
|
|
||||||
_NoOpIcon(
|
|
||||||
icon: Icons.calendar_today,
|
|
||||||
color: colors.accent,
|
|
||||||
metrics: metrics,
|
|
||||||
),
|
|
||||||
_NoOpIcon(
|
|
||||||
icon: Icons.wb_sunny_outlined,
|
|
||||||
color: colors.accent,
|
|
||||||
metrics: metrics,
|
|
||||||
),
|
|
||||||
SizedBox(width: metrics.actionGap),
|
|
||||||
],
|
|
||||||
_Badge(
|
_Badge(
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
child: RewardIcon(
|
child: RewardIcon(
|
||||||
|
|
@ -111,6 +113,7 @@ class TaskTimelineCard extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
@ -284,6 +287,55 @@ class _CardText extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _QuickActions extends StatelessWidget {
|
||||||
|
const _QuickActions({
|
||||||
|
required this.color,
|
||||||
|
required this.metrics,
|
||||||
|
required this.visible,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
final _CardMetrics metrics;
|
||||||
|
final bool visible;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ExcludeSemantics(
|
||||||
|
excluding: !visible,
|
||||||
|
child: IgnorePointer(
|
||||||
|
ignoring: !visible,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: visible ? 1 : 0,
|
||||||
|
duration: const Duration(milliseconds: 120),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_NoOpIcon(icon: Icons.check, color: color, metrics: metrics),
|
||||||
|
_NoOpIcon(
|
||||||
|
icon: Icons.arrow_forward,
|
||||||
|
color: color,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
_NoOpIcon(
|
||||||
|
icon: Icons.calendar_today,
|
||||||
|
color: color,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
_NoOpIcon(
|
||||||
|
icon: Icons.wb_sunny_outlined,
|
||||||
|
color: color,
|
||||||
|
metrics: metrics,
|
||||||
|
),
|
||||||
|
SizedBox(width: metrics.actionGap),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _NoOpIcon extends StatelessWidget {
|
class _NoOpIcon extends StatelessWidget {
|
||||||
const _NoOpIcon({
|
const _NoOpIcon({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
/// Tests Widget behavior in the FocusFlow Flutter app.
|
/// Tests Widget behavior in the FocusFlow Flutter app.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:scheduler_core/scheduler_core.dart';
|
import 'package:scheduler_core/scheduler_core.dart';
|
||||||
|
|
@ -124,12 +125,29 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
|
expect(
|
||||||
|
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final gesture = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||||
|
await gesture.addPointer(
|
||||||
|
location: tester.getCenter(find.byKey(const ValueKey('narrow-actions'))),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
tester.widget<AnimatedOpacity>(find.byType(AnimatedOpacity)).opacity,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
final button = tester.widget<IconButton>(
|
final button = tester.widget<IconButton>(
|
||||||
find.widgetWithIcon(IconButton, Icons.arrow_forward),
|
find.widgetWithIcon(IconButton, Icons.arrow_forward),
|
||||||
);
|
);
|
||||||
expect(button.iconSize, lessThan(18));
|
expect(button.iconSize, lessThan(18));
|
||||||
expect(button.constraints?.maxWidth, lessThan(32));
|
expect(button.constraints?.maxWidth, lessThan(32));
|
||||||
|
|
||||||
|
await gesture.removePointer();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('free slot text scales inside a short task bubble', (
|
testWidgets('free slot text scales inside a short task bubble', (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue