fix(todo): the add button slides away while scrolling, and stays put behind quick add

Every tab's screen (TabScreen: Inbox, Today, Upcoming, a project's,
filters' and labels') hands its add button to halkyon_ui's
ScrollHidingFab, as Learn's screens do: it slides away while the tasks
scroll on down and comes back as they scroll up.

Its scaffold no longer resizes for the keyboard: opening quick add, the
add button and anything at the screen's foot jumped up by the keyboard's
height behind the bar, which floats above the keyboard by itself. No tab
has a field of its own in its body (search, the editors and the pickers
are screens, sheets or dialogs of their own). A task's sheet, where a
sub-task is added, keeps still already: its foot is its scaffold's bar.

Whats-New: The add button slides away while you scroll down, and the screen behind Add task no longer jumps with the keyboard.
Whats-New-De: Der Hinzufügen-Knopf weicht beim Herunterscrollen aus, und der Bildschirm hinter Aufgabe hinzufügen springt nicht mehr mit der Tastatur.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-07 23:08:55 +01:00
commit 90f9415e72
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 149 additions and 44 deletions

View file

@ -48,50 +48,12 @@ class const TabScreen({
},
child: TopGradient(
background: Theme.of(context).colorScheme.surface,
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
systemOverlayStyle: TopGradient.overlayStyle(context),
leading: choosing
? IconButton(
tooltip: l10n.selectionEnd,
icon: const Icon(LucideIcons.arrowLeft),
onPressed: selection.end,
)
: null,
title: switch (subtitle) {
final subtitle? when !choosing => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
_ => Text(
choosing ? l10n.selectionCount(selection.ids.length) : title,
),
},
actions: choosing
? const []
: [
if (search) const SearchButton(),
...actions,
const SizedBox(width: 4),
],
),
body: body,
bottomNavigationBar: choosing
? TaskSelectionBar(selection: selection)
: null,
// Clear of the navigation bar floating over the foot on a phone.
floatingActionButton: switch (floatingActionButton) {
// The add button slides away while the tasks scroll on down, and
// back as they scroll up, as Learn's (the developer's review, 7 Nov
// 2026).
child: ScrollHidingFab(
fab: switch (floatingActionButton) {
// Clear of the navigation bar floating over the foot on a phone.
final button? when !choosing => Padding(
padding: EdgeInsets.only(
bottom: FloatingNavigationBar.coverOf(context),
@ -100,6 +62,55 @@ class const TabScreen({
),
_ => null,
},
builder: (context, fab) => Scaffold(
backgroundColor: Colors.transparent,
// Quick add's bar and the keyboard float over the tasks, which
// stay where they are (the developer's review, 7 Nov 2026: they
// jumped up by the keyboard's height behind the bar); a field of
// the screen's own is in a sheet or a screen of its own.
resizeToAvoidBottomInset: false,
appBar: AppBar(
backgroundColor: Colors.transparent,
systemOverlayStyle: TopGradient.overlayStyle(context),
leading: choosing
? IconButton(
tooltip: l10n.selectionEnd,
icon: const Icon(LucideIcons.arrowLeft),
onPressed: selection.end,
)
: null,
title: switch (subtitle) {
final subtitle? when !choosing => Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title),
Text(
subtitle,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
_ => Text(
choosing ? l10n.selectionCount(selection.ids.length) : title,
),
},
actions: choosing
? const []
: [
if (search) const SearchButton(),
...actions,
const SizedBox(width: 4),
],
),
body: body,
bottomNavigationBar: choosing
? TaskSelectionBar(selection: selection)
: null,
floatingActionButton: fab,
),
),
),
);

View file

@ -2749,6 +2749,100 @@ void main() {
await tester.pumpAndSettle();
}
testWidgets('the add button slides away as the tasks scroll on down '
"and comes back as they scroll up, on every tab, as Learn's "
"(the developer's review, 7 Nov 2026)", (tester) async {
await start(tester, usedBefore: true);
await tester.runAsync(() async {
for (var index = 0; index < 30; index++) {
await tasks().add(
TaskContent(
title: 'Task $index',
projectId: inbox(),
position: index.toDouble(),
date: TaskDate(Day.of(DateTime.now())),
),
);
}
});
taskRows.bringRemoteChanges();
await tester.pumpAndSettle();
bool shown() =>
tester
.widget<AnimatedSlide>(
find.ancestor(
of: find.byType(AddTaskButton),
matching: find.byType(AnimatedSlide),
),
)
.offset ==
Offset.zero;
for (final tab in ['Inbox', 'Today', 'Upcoming']) {
await goTab(tester, tab);
expect(shown(), isTrue, reason: tab);
await tester.drag(find.text('Task 3'), const Offset(0, -300));
await tester.pumpAndSettle();
expect(shown(), isFalse, reason: tab);
await tester.drag(find.text('Task 8'), const Offset(0, 200));
await tester.pumpAndSettle();
expect(shown(), isTrue, reason: tab);
}
});
testWidgets('behind the bar and the keyboard the tasks stay still, on '
"every tab and under a task's sheet (the developer's review, 7 "
"Nov 2026: they jumped up by the keyboard's height)", (
tester,
) async {
await start(tester, usedBefore: true);
await seed(
tester,
TaskContent(
title: 'Call mum',
projectId: inbox(),
date: TaskDate(Day.of(DateTime.now())),
),
);
addTearDown(tester.view.resetViewInsets);
for (final tab in ['Inbox', 'Today', 'Upcoming']) {
await goTab(tester, tab);
final before = tester.getRect(find.text('Call mum'));
final button = find.byType(AddTaskButton);
final under = tester.getRect(button);
await tester.tap(find.byTooltip('Add task'));
await tester.pumpAndSettle();
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
await tester.pumpAndSettle();
expect(find.byType(QuickAddBar), findsOneWidget);
expect(tester.getRect(find.text('Call mum')), before, reason: tab);
expect(tester.getRect(button), under, reason: tab);
tester.view.resetViewInsets();
await tester.pumpAndSettle();
await tester.tapAt(const Offset(20, 20));
await tester.pumpAndSettle();
}
// A sub-task added from the task's sheet: the sheet stays too.
await goTab(tester, 'Inbox');
await tester.tap(find.text('Call mum'));
await tester.pumpAndSettle();
final bar = tester.getRect(find.byKey(AddCommentBar.barKey));
await tester.ensureVisible(find.text('Add sub-task'));
await tester.pumpAndSettle();
final add = tester.getRect(find.text('Add sub-task'));
await tester.tap(find.text('Add sub-task'));
await tester.pumpAndSettle();
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
await tester.pumpAndSettle();
expect(find.byType(QuickAddBar), findsOneWidget);
expect(tester.getRect(find.byKey(AddCommentBar.barKey)), bar);
expect(tester.getRect(find.text('Add sub-task')), add);
});
testWidgets("a small bar docked just above the keyboard, as Todoist's, "
'the list still in view above it', (tester) async {
await start(tester, usedBefore: true);