fix(todo): the Plan's button before search, and its sheet without the navigation bar
Today's and Upcoming's Plan button stood between search and Display; it stands before the three every tab has now (TabScreen's `leadingActions`). On a phone, where the Plan opens over the hours' foot, its icon is Lucide's panel-bottom-open (panel-bottom-close while open); beside the hours on wider windows it keeps the panel at the right. While a phone's Plan is open, the navigation bar steps aside, as it does while tasks are being chosen: it floated over the Plan's tasks, and the Plan can't be a modal sheet, its rows being dragged out onto the hours. `NavigationBarHold`, kept by TodoShell, holds it away while anyone asks; closing the Plan, or back, which closes it first, brings it back. The Plan's heading put its projects and close button after a spacer of one share and a flexible of four, leaving a fifth of the room after them; they end at its edge now, as the rest of it. Whats-New: The Plan's button stands before search, and while the Plan is open the navigation bar makes way for it. Whats-New-De: Der Plan-Knopf steht vor der Suche, und solange der Plan offen ist, macht ihm die Navigationsleiste Platz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
90f9415e72
commit
ccdf22b936
8 changed files with 251 additions and 61 deletions
|
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Who holds the navigation bar away below medium windows (the developer's
|
||||||
|
/// review, 7 Nov 2026): a phone's Plan over the hours' foot, its rows
|
||||||
|
/// dragged onto them, so it can't be a modal sheet, yet the bar floated
|
||||||
|
/// over its tasks. Held by anyone, the bar steps aside; released by all,
|
||||||
|
/// it comes back. `TodoShell` keeps it, and [NavigationBarHoldScope] hands
|
||||||
|
/// it down.
|
||||||
|
class NavigationBarHold() extends ChangeNotifier {
|
||||||
|
final _holders = <Object>{};
|
||||||
|
bool _disposed = false;
|
||||||
|
|
||||||
|
/// Whether anyone holds the bar away.
|
||||||
|
bool get held => _holders.isNotEmpty;
|
||||||
|
|
||||||
|
/// [holder] holds the bar away, or no longer where [hold] is false.
|
||||||
|
void set(Object holder, {required bool hold}) {
|
||||||
|
if (_disposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final changed = hold ? _holders.add(holder) : _holders.remove(holder);
|
||||||
|
if (changed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_disposed = true;
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hands [notifier] down to the tabs.
|
||||||
|
class const NavigationBarHoldScope({
|
||||||
|
required final NavigationBarHold notifier,
|
||||||
|
required super.child,
|
||||||
|
super.key,
|
||||||
|
}) extends InheritedWidget {
|
||||||
|
/// The hold above [context], if any; nothing rebuilds as it changes.
|
||||||
|
static NavigationBarHold? maybeOf(BuildContext context) =>
|
||||||
|
context.getInheritedWidgetOfExactType<NavigationBarHoldScope>()?.notifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool updateShouldNotify(NavigationBarHoldScope oldWidget) =>
|
||||||
|
notifier != oldWidget.notifier;
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||||
|
import 'package:halkyon_todo_flutter/src/tabs/navigation_bar_hold.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/tabs/pane_widths.dart';
|
import 'package:halkyon_todo_flutter/src/tabs/pane_widths.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/tabs/plan_panel.dart';
|
import 'package:halkyon_todo_flutter/src/tabs/plan_panel.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/tabs/side_pane.dart';
|
import 'package:halkyon_todo_flutter/src/tabs/side_pane.dart';
|
||||||
|
|
@ -15,7 +16,8 @@ import 'package:material_ui/material_ui.dart';
|
||||||
/// app bar's button shows or hides it, beside the hours from a medium width
|
/// app bar's button shows or hides it, beside the hours from a medium width
|
||||||
/// on (remembered with the person's settings, for every calendar alike),
|
/// on (remembered with the person's settings, for every calendar alike),
|
||||||
/// over their foot on a phone, stepping down out of the way while one of
|
/// over their foot on a phone, stepping down out of the way while one of
|
||||||
/// its rows is dragged onto an hour.
|
/// its rows is dragged onto an hour, the navigation bar away while it is
|
||||||
|
/// open there.
|
||||||
mixin PlanHost<T extends StatefulWidget> on State<T> {
|
mixin PlanHost<T extends StatefulWidget> on State<T> {
|
||||||
/// The projects the Plan shows, for as long as the app runs (none: all).
|
/// The projects the Plan shows, for as long as the app runs (none: all).
|
||||||
Set<String> _projects = const {};
|
Set<String> _projects = const {};
|
||||||
|
|
@ -42,11 +44,41 @@ mixin PlanHost<T extends StatefulWidget> on State<T> {
|
||||||
/// Whether the phone's Plan is open, where the add button makes way.
|
/// Whether the phone's Plan is open, where the add button makes way.
|
||||||
bool get planSheetOpen => !_wide && _sheet;
|
bool get planSheetOpen => !_wide && _sheet;
|
||||||
|
|
||||||
|
/// The navigation bar held away while the phone's Plan is open, its
|
||||||
|
/// rows free of it (the developer's review, 7 Nov 2026).
|
||||||
|
NavigationBarHold? _hold;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
_hold = NavigationBarHoldScope.maybeOf(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The phone's Plan opened or closed, the navigation bar going or
|
||||||
|
/// coming with it.
|
||||||
|
void _setSheet(bool open) {
|
||||||
|
setState(() => _sheet = open);
|
||||||
|
_syncBar();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncBar() => _hold?.set(this, hold: planSheetOpen);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
final hold = _hold;
|
||||||
|
// Not while the tree is being taken down, which the bar's frame is part
|
||||||
|
// of: just after.
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback(
|
||||||
|
(_) => hold?.set(this, hold: false),
|
||||||
|
);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
/// The Plan shown or hidden: beside the hours, remembered with the
|
/// The Plan shown or hidden: beside the hours, remembered with the
|
||||||
/// person's settings; on a phone, its sheet.
|
/// person's settings; on a phone, its sheet.
|
||||||
Future<void> _togglePlan(TodoState state) async {
|
Future<void> _togglePlan(TodoState state) async {
|
||||||
if (!_wide) {
|
if (!_wide) {
|
||||||
setState(() => _sheet = !_sheet);
|
_setSheet(!_sheet);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final todo = PersonScope.of<TodoPerson>(context);
|
final todo = PersonScope.of<TodoPerson>(context);
|
||||||
|
|
@ -65,9 +97,14 @@ mixin PlanHost<T extends StatefulWidget> on State<T> {
|
||||||
message: shown ? l10n.planHide : l10n.planShow,
|
message: shown ? l10n.planHide : l10n.planShow,
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
key: planButtonKey,
|
key: planButtonKey,
|
||||||
icon: Icon(
|
// Where the Plan opens: beside the hours, or over their foot on a
|
||||||
shown ? LucideIcons.panelRightClose : LucideIcons.panelRight,
|
// phone (the developer's review, 7 Nov 2026).
|
||||||
),
|
icon: Icon(switch ((_wide, shown)) {
|
||||||
|
(true, true) => LucideIcons.panelRightClose,
|
||||||
|
(true, false) => LucideIcons.panelRight,
|
||||||
|
(false, true) => LucideIcons.panelBottomClose,
|
||||||
|
(false, false) => LucideIcons.panelBottomOpen,
|
||||||
|
}),
|
||||||
label: Text(l10n.planButton(planned)),
|
label: Text(l10n.planButton(planned)),
|
||||||
onPressed: () => _togglePlan(state),
|
onPressed: () => _togglePlan(state),
|
||||||
),
|
),
|
||||||
|
|
@ -91,6 +128,7 @@ mixin PlanHost<T extends StatefulWidget> on State<T> {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
_syncBar();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -113,47 +151,56 @@ mixin PlanHost<T extends StatefulWidget> on State<T> {
|
||||||
final cover = FloatingNavigationBar.coverOf(context);
|
final cover = FloatingNavigationBar.coverOf(context);
|
||||||
final height = constraints.maxHeight * 0.5;
|
final height = constraints.maxHeight * 0.5;
|
||||||
const heading = 56.0;
|
const heading = 56.0;
|
||||||
return Stack(
|
// Back closes the Plan first, the navigation bar away meanwhile.
|
||||||
children: [
|
return PopScope(
|
||||||
Positioned.fill(child: grid(_grid, _sheet ? height : 0)),
|
canPop: !_sheet,
|
||||||
if (_sheet)
|
onPopInvokedWithResult: (popped, _) {
|
||||||
Positioned(
|
if (!popped && _sheet) {
|
||||||
left: 0,
|
_setSheet(false);
|
||||||
right: 0,
|
}
|
||||||
bottom: 0,
|
},
|
||||||
height: height + cover,
|
child: Stack(
|
||||||
child: AnimatedSlide(
|
children: [
|
||||||
duration: const Duration(milliseconds: 200),
|
Positioned.fill(child: grid(_grid, _sheet ? height : 0)),
|
||||||
curve: Curves.easeOut,
|
if (_sheet)
|
||||||
offset: Offset(
|
Positioned(
|
||||||
0,
|
left: 0,
|
||||||
_lowered ? (height - heading) / (height + cover) : 0,
|
right: 0,
|
||||||
),
|
bottom: 0,
|
||||||
child: Material(
|
height: height + cover,
|
||||||
key: planSheetKey,
|
child: AnimatedSlide(
|
||||||
elevation: 8,
|
duration: const Duration(milliseconds: 200),
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
curve: Curves.easeOut,
|
||||||
clipBehavior: Clip.antiAlias,
|
offset: Offset(
|
||||||
shape: const RoundedRectangleBorder(
|
0,
|
||||||
borderRadius: BorderRadius.vertical(
|
_lowered ? (height - heading) / (height + cover) : 0,
|
||||||
top: Radius.circular(16),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
child: PlanPanel(
|
child: Material(
|
||||||
projectIds: _projects,
|
key: planSheetKey,
|
||||||
onProjects: projects,
|
elevation: 8,
|
||||||
onClose: () => setState(() => _sheet = false),
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
onHeaderTap: () => setState(() => _lowered = false),
|
clipBehavior: Clip.antiAlias,
|
||||||
onDragging: (dragging) {
|
shape: const RoundedRectangleBorder(
|
||||||
if (dragging != _lowered) {
|
borderRadius: BorderRadius.vertical(
|
||||||
setState(() => _lowered = dragging);
|
top: Radius.circular(16),
|
||||||
}
|
),
|
||||||
},
|
),
|
||||||
|
child: PlanPanel(
|
||||||
|
projectIds: _projects,
|
||||||
|
onProjects: projects,
|
||||||
|
onClose: () => _setSheet(false),
|
||||||
|
onHeaderTap: () => setState(() => _lowered = false),
|
||||||
|
onDragging: (dragging) {
|
||||||
|
if (dragging != _lowered) {
|
||||||
|
setState(() => _lowered = dragging);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -231,20 +231,24 @@ class const _PlanHeading({
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
// At the end, beside the close button (the developer's review,
|
||||||
Flexible(
|
// 7 Nov 2026: a spacer and a flexible of four shares left a
|
||||||
flex: 4,
|
// fifth of the room after them), shortened where it is long.
|
||||||
child: TextButton.icon(
|
Expanded(
|
||||||
key: PlanPanel.projectsKey,
|
child: Align(
|
||||||
iconAlignment: IconAlignment.end,
|
alignment: AlignmentDirectional.centerEnd,
|
||||||
icon: const Icon(LucideIcons.chevronDown, size: 18),
|
child: TextButton.icon(
|
||||||
label: Text(
|
key: PlanPanel.projectsKey,
|
||||||
label,
|
iconAlignment: IconAlignment.end,
|
||||||
maxLines: 1,
|
icon: const Icon(LucideIcons.chevronDown, size: 18),
|
||||||
overflow: TextOverflow.ellipsis,
|
label: Text(
|
||||||
|
label,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
onPressed: () =>
|
||||||
|
_pickProjects(context, projectIds, onProjects),
|
||||||
),
|
),
|
||||||
onPressed: () =>
|
|
||||||
_pickProjects(context, projectIds, onProjects),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (onClose case final close?)
|
if (onClose case final close?)
|
||||||
|
|
|
||||||
|
|
@ -22,9 +22,14 @@ class const TabScreen({
|
||||||
/// The tab's content.
|
/// The tab's content.
|
||||||
required final Widget body,
|
required final Widget body,
|
||||||
|
|
||||||
/// The app bar's actions.
|
/// The app bar's actions, after search.
|
||||||
final List<Widget> actions = const [],
|
final List<Widget> actions = const [],
|
||||||
|
|
||||||
|
/// The app bar's actions before search, the tab's own beside the ones
|
||||||
|
/// every tab has (Today's and Upcoming's Plan, the developer's review, 7
|
||||||
|
/// Nov 2026).
|
||||||
|
final List<Widget> leadingActions = const [],
|
||||||
|
|
||||||
/// The screen's button, above the navigation bar.
|
/// The screen's button, above the navigation bar.
|
||||||
final Widget? floatingActionButton,
|
final Widget? floatingActionButton,
|
||||||
|
|
||||||
|
|
@ -100,6 +105,7 @@ class const TabScreen({
|
||||||
actions: choosing
|
actions: choosing
|
||||||
? const []
|
? const []
|
||||||
: [
|
: [
|
||||||
|
...leadingActions,
|
||||||
if (search) const SearchButton(),
|
if (search) const SearchButton(),
|
||||||
...actions,
|
...actions,
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
|
|
|
||||||
|
|
@ -67,8 +67,8 @@ class _TodayTabState() extends State<TodayTab> with PlanHost<TodayTab> {
|
||||||
return TabScreen(
|
return TabScreen(
|
||||||
title: l10n.shellTabToday,
|
title: l10n.shellTabToday,
|
||||||
subtitle: state.loaded && count > 0 ? l10n.todayCount(count) : null,
|
subtitle: state.loaded && count > 0 ? l10n.todayCount(count) : null,
|
||||||
|
leadingActions: [if (calendar) planButton(context, state, today)],
|
||||||
actions: [
|
actions: [
|
||||||
if (calendar) planButton(context, state, today),
|
|
||||||
DisplayButton(target: todayDisplay),
|
DisplayButton(target: todayDisplay),
|
||||||
const SelectTasksMenu(),
|
const SelectTasksMenu(),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
|
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_selection.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_selection.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_sheet.dart';
|
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_sheet.dart';
|
||||||
|
import 'package:halkyon_todo_flutter/src/tabs/navigation_bar_hold.dart';
|
||||||
import 'package:halkyon_todo_flutter/src/tabs/today_icon.dart';
|
import 'package:halkyon_todo_flutter/src/tabs/today_icon.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
@ -29,9 +30,13 @@ class _TodoShellState() extends State<TodoShell> {
|
||||||
/// what to do with them takes the navigation bar's place.
|
/// what to do with them takes the navigation bar's place.
|
||||||
final _selection = TaskSelection();
|
final _selection = TaskSelection();
|
||||||
|
|
||||||
|
/// Whoever holds the navigation bar away (a phone's Plan).
|
||||||
|
final _hold = NavigationBarHold();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_selection.dispose();
|
_selection.dispose();
|
||||||
|
_hold.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -42,13 +47,13 @@ class _TodoShellState() extends State<TodoShell> {
|
||||||
return TaskSelectionScope(
|
return TaskSelectionScope(
|
||||||
notifier: _selection,
|
notifier: _selection,
|
||||||
child: ListenableBuilder(
|
child: ListenableBuilder(
|
||||||
listenable: _selection,
|
listenable: Listenable.merge([_selection, _hold]),
|
||||||
builder: (context, child) => WhatsNewOnUpdate(
|
builder: (context, child) => WhatsNewOnUpdate(
|
||||||
child: NavigationFrame(
|
child: NavigationFrame(
|
||||||
appName: l10n.appTitle,
|
appName: l10n.appTitle,
|
||||||
icon: todoIcon,
|
icon: todoIcon,
|
||||||
selectedIndex: shell.currentIndex,
|
selectedIndex: shell.currentIndex,
|
||||||
hidesNavigationBar: _selection.active,
|
hidesNavigationBar: _selection.active || _hold.held,
|
||||||
// A tab chosen again goes back to its first screen; choosing tasks
|
// A tab chosen again goes back to its first screen; choosing tasks
|
||||||
// ends with the tab.
|
// ends with the tab.
|
||||||
onSelected: (index) {
|
onSelected: (index) {
|
||||||
|
|
@ -88,7 +93,10 @@ class _TodoShellState() extends State<TodoShell> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// From expanded windows on, a task opens beside the list (D2).
|
// From expanded windows on, a task opens beside the list (D2).
|
||||||
child: TaskPane(child: shell),
|
child: NavigationBarHoldScope(
|
||||||
|
notifier: _hold,
|
||||||
|
child: TaskPane(child: shell),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,11 @@ class _UpcomingTabState()
|
||||||
void select(Day day) => setState(() => _selected = day);
|
void select(Day day) => setState(() => _selected = day);
|
||||||
return TabScreen(
|
return TabScreen(
|
||||||
title: l10n.shellTabUpcoming,
|
title: l10n.shellTabUpcoming,
|
||||||
actions: [
|
leadingActions: [
|
||||||
if (upcoming.layout == UpcomingLayout.threeDay)
|
if (upcoming.layout == UpcomingLayout.threeDay)
|
||||||
planButton(context, state, today),
|
planButton(context, state, today),
|
||||||
|
],
|
||||||
|
actions: [
|
||||||
DisplayButton(target: upcomingDisplay),
|
DisplayButton(target: upcomingDisplay),
|
||||||
const SelectTasksMenu(),
|
const SelectTasksMenu(),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1963,6 +1963,12 @@ void main() {
|
||||||
expect(find.byKey(UpcomingTab.planKey), findsNothing);
|
expect(find.byKey(UpcomingTab.planKey), findsNothing);
|
||||||
await threeDay(tester);
|
await threeDay(tester);
|
||||||
expect(find.byKey(UpcomingTab.planKey), findsOneWidget);
|
expect(find.byKey(UpcomingTab.planKey), findsOneWidget);
|
||||||
|
// Before search, Display and the menu every tab has (the
|
||||||
|
// developer's review, 7 Nov 2026).
|
||||||
|
expect(
|
||||||
|
tester.getTopRight(find.byKey(UpcomingTab.planKey)).dx,
|
||||||
|
lessThanOrEqualTo(tester.getTopLeft(find.byTooltip('Search')).dx),
|
||||||
|
);
|
||||||
tester.view.physicalSize = const Size(1400, 900);
|
tester.view.physicalSize = const Size(1400, 900);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('No date'), findsOneWidget);
|
expect(find.text('No date'), findsOneWidget);
|
||||||
|
|
@ -2690,6 +2696,76 @@ void main() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('on a phone, Plan stands before search, pointing down where '
|
||||||
|
'it opens; open, the navigation bar steps aside until it is '
|
||||||
|
'closed or back is pressed, its projects and close button at its '
|
||||||
|
"end (the developer's review, 7 Nov 2026)", (tester) async {
|
||||||
|
await start(tester, usedBefore: true);
|
||||||
|
// A wide phone, room to spare beside the heading's projects.
|
||||||
|
tester.view.physicalSize = const Size(560, 900);
|
||||||
|
await calendar(tester);
|
||||||
|
final plan = find.byKey(TodayTab.planKey);
|
||||||
|
final search = tester.getTopLeft(find.byTooltip('Search')).dx;
|
||||||
|
expect(tester.getTopRight(plan).dx, lessThanOrEqualTo(search));
|
||||||
|
expect(
|
||||||
|
search,
|
||||||
|
lessThan(tester.getTopLeft(find.byTooltip('Display')).dx),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: plan,
|
||||||
|
matching: find.byIcon(LucideIcons.panelBottomOpen),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.byType(NavigationBar), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(plan);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final sheet = find.byKey(TodayTab.planSheetKey);
|
||||||
|
expect(sheet, findsOneWidget);
|
||||||
|
expect(find.byType(NavigationBar), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: plan,
|
||||||
|
matching: find.byIcon(LucideIcons.panelBottomClose),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
tester.getBottomLeft(sheet).dy,
|
||||||
|
900,
|
||||||
|
reason: 'down to the foot, no bar below it',
|
||||||
|
);
|
||||||
|
|
||||||
|
final close = find.ancestor(
|
||||||
|
of: find.descendant(
|
||||||
|
of: sheet,
|
||||||
|
matching: find.byTooltip('Hide the Plan'),
|
||||||
|
),
|
||||||
|
matching: find.byType(IconButton),
|
||||||
|
);
|
||||||
|
expect(tester.getTopRight(close).dx, tester.getTopRight(sheet).dx - 4);
|
||||||
|
expect(
|
||||||
|
tester.getTopRight(find.byKey(PlanPanel.projectsKey)).dx,
|
||||||
|
moreOrLessEquals(tester.getTopLeft(close).dx, epsilon: 1),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(close);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(sheet, findsNothing);
|
||||||
|
expect(find.byType(NavigationBar), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(plan);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byType(NavigationBar), findsNothing);
|
||||||
|
await tester.binding.handlePopRoute();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(sheet, findsNothing);
|
||||||
|
expect(find.byType(NavigationBar), findsOneWidget);
|
||||||
|
expect(find.byKey(TodayTab.planKey), findsOneWidget, reason: 'stays');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('on a phone, Plan opens a sheet over the day; a row held '
|
testWidgets('on a phone, Plan opens a sheet over the day; a row held '
|
||||||
'there lowers it out of the way and dropped on 14:00 is planned '
|
'there lowers it out of the way and dropped on 14:00 is planned '
|
||||||
'then', (tester) async {
|
'then', (tester) async {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue