feat(todo): a sub-task opens in its task's sheet, back returning to it
A sub-task tapped in a task's sheet opened a sheet of its own over it, so Grocery List > Dairy > Milk stacked three. It now takes the sheet's place, as the steps of the sheet's path already did (TaskTile's onOpen), and the sheet keeps the tasks it showed: the system's back returns to the last. Back is caught with a BackButtonListener, not a PopScope, which would also hold back a tap beside the sheet, a swipe and Escape: those close the sheet whole. A picker open over the sheet is closed by back first. Whats-New: A sub-task opens in its task's sheet instead of over it; back returns to the task. Whats-New-De: Eine Unteraufgabe öffnet sich im Blatt ihrer Aufgabe statt darüber; Zurück führt zur Aufgabe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
a71d80fc2d
commit
e11ed481ce
3 changed files with 94 additions and 8 deletions
|
|
@ -145,7 +145,11 @@ class const _SheetScroll({
|
|||
controller != oldWidget.controller;
|
||||
}
|
||||
|
||||
/// A sheet's task, which a step of its path replaces in place.
|
||||
/// A sheet's task, which a step of its path or a sub-task replaces in place
|
||||
/// (the developer's review, 4 Nov 2026: a sub-task had opened a sheet over
|
||||
/// the sheet). The tasks it showed before are kept, so the system's back
|
||||
/// returns to the last; every other way of closing it (a tap beside it, a
|
||||
/// swipe down, Escape) closes it whole.
|
||||
class const _TaskSheetHost({required final String id}) extends StatefulWidget {
|
||||
@override
|
||||
State<_TaskSheetHost> createState() => _TaskSheetHostState();
|
||||
|
|
@ -154,12 +158,45 @@ class const _TaskSheetHost({required final String id}) extends StatefulWidget {
|
|||
class _TaskSheetHostState() extends State<_TaskSheetHost> {
|
||||
late var _id = widget.id;
|
||||
|
||||
/// The tasks shown before [_id], the last most recently.
|
||||
final _history = <String>[];
|
||||
|
||||
/// Shows [id]; one shown before is gone back to, the steps after it
|
||||
/// forgotten.
|
||||
void _show(String id) {
|
||||
if (id == _id) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
final before = _history.indexOf(id);
|
||||
if (before >= 0) {
|
||||
_history.removeRange(before, _history.length);
|
||||
} else {
|
||||
_history.add(_id);
|
||||
}
|
||||
_id = id;
|
||||
});
|
||||
}
|
||||
|
||||
/// Back from the system: the task shown before, while there is one and
|
||||
/// nothing is open over the sheet (a picker, which back closes first).
|
||||
Future<bool> _back() async {
|
||||
if (_history.isEmpty || !(ModalRoute.of(context)?.isCurrent ?? false)) {
|
||||
return false;
|
||||
}
|
||||
setState(() => _id = _history.removeLast());
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _TaskSheet(
|
||||
key: ValueKey(_id),
|
||||
id: _id,
|
||||
onOpen: (id) => setState(() => _id = id),
|
||||
);
|
||||
Widget build(BuildContext context) {
|
||||
final sheet = _TaskSheet(key: ValueKey(_id), id: _id, onOpen: _show);
|
||||
// Not a PopScope: that would hold back a tap beside the sheet and
|
||||
// Escape too, which close it whole. Back alone goes through the router.
|
||||
return Router.maybeOf(context) == null
|
||||
? sheet
|
||||
: BackButtonListener(onBackButtonPressed: _back, child: sheet);
|
||||
}
|
||||
}
|
||||
|
||||
/// The task open beside the list from expanded windows on (ADR 0033, D2,
|
||||
|
|
@ -759,7 +796,11 @@ class _TaskSheetState() extends State<_TaskSheet> {
|
|||
),
|
||||
for (final sub in subTasks)
|
||||
if (!(content.hideDoneSubTasks && sub.content.completed))
|
||||
TaskTile(task: sub, selectable: false),
|
||||
TaskTile(
|
||||
task: sub,
|
||||
selectable: false,
|
||||
onOpen: (id) => _open(context, id),
|
||||
),
|
||||
],
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ class const TaskTile({
|
|||
final bool selectable = true,
|
||||
final bool later = false,
|
||||
final TaskRowPosition position = TaskRowPosition.alone,
|
||||
|
||||
/// Opens the task in place of the one showing, as a sheet's sub-tasks
|
||||
/// do; by default a tap opens the task's sheet.
|
||||
final ValueChanged<String>? onOpen,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
|
|
@ -252,7 +256,10 @@ class const TaskTile({
|
|||
),
|
||||
onTap: selection != null && selection.active
|
||||
? () => selection.toggle(task.id)
|
||||
: () => openTaskSheet(context, task.id),
|
||||
: switch (onOpen) {
|
||||
final open? => () => open(task.id),
|
||||
null => () => openTaskSheet(context, task.id),
|
||||
},
|
||||
);
|
||||
if (selection == null) {
|
||||
return _boxed(context, tile);
|
||||
|
|
|
|||
|
|
@ -2184,6 +2184,44 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("a sub-task opens in its task's sheet, not in another over "
|
||||
'it; back returns to the task, a tap beside closes it (the '
|
||||
"developer's review, 4 Nov 2026)", (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
final list = await tester.runAsync(
|
||||
() =>
|
||||
tasks().add(TaskContent(title: 'Grocery List', projectId: inbox())),
|
||||
);
|
||||
final dairy = await tester.runAsync(
|
||||
() => tasks().add(
|
||||
TaskContent(title: 'Dairy', projectId: inbox(), parentId: list),
|
||||
),
|
||||
);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(title: 'Milk', projectId: inbox(), parentId: dairy),
|
||||
);
|
||||
await goTab(tester, 'Inbox');
|
||||
|
||||
await tester.tap(find.text('Grocery List').first);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Dairy').last);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Milk').last);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(BottomSheet), findsOneWidget);
|
||||
expect(find.widgetWithText(TextField, 'Milk'), findsOneWidget);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(BottomSheet), findsOneWidget);
|
||||
expect(find.widgetWithText(TextField, 'Dairy'), findsOneWidget);
|
||||
|
||||
await tester.tapAt(const Offset(20, 20));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(BottomSheet), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("a task's menu says when it was added, duplicates it with its "
|
||||
'sub-tasks and hides its done ones, on every device', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
|
|
|
|||
Loading…
Reference in a new issue