feat(todo): quick add a small bar docked above the keyboard, as Todoist's
The developer's review of 3 Nov 2026 (ADR 0033, D23): in place of the tall sheet with its title and Add button, a rounded bar just above the keyboard, the list still in view behind a lighter barrier: the title, then a row of boxed buttons (+ for the description and the deadline, the date, the priority, the labels, the project), each its icon alone until something is set, and a send button. Its buttons show from the start rather than once typing begins (D15 had them come with the first letter). Where sheets are dialogs, the same bar in a dialog. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c7ad78f88d
commit
8438caa489
2 changed files with 301 additions and 173 deletions
|
|
@ -19,23 +19,24 @@ import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// Quick add (ADR 0033, D8): a task's title, and chips for its date,
|
/// Quick add (ADR 0033, D8): a task's title, and buttons for its date,
|
||||||
/// priority, project and labels, and a description where asked for. It
|
/// priority, labels and project, and a description where asked for. It
|
||||||
/// goes into [projectId] (the Inbox where none is given), [sectionId] and
|
/// goes into [projectId] (the Inbox where none is given), [sectionId] and
|
||||||
/// under [parentId] where given, dated [day] where given (at [time], minutes
|
/// under [parentId] where given, dated [day] where given (at [time], minutes
|
||||||
/// after midnight, for [duration] where given: a slot of the 3-day grid,
|
/// after midnight, for [duration] where given: a slot of the 3-day grid,
|
||||||
/// D17), last among its
|
/// D17), last among its siblings, or at [spot] where the add button was
|
||||||
/// siblings, or at [spot] where the add button was dropped (D16), each
|
/// dropped (D16), each next one below the one before. It stays for the next
|
||||||
/// next one below the one before. The sheet stays for the next task; going
|
/// task; going back, or a tap beside it, closes it.
|
||||||
/// back closes it.
|
|
||||||
///
|
///
|
||||||
/// It shows the title alone at first, its chips once typing starts, and its
|
/// On a phone a small bar docked just above the keyboard, as Todoist's,
|
||||||
/// description from its + or the down arrow (the developer's, 1 Nov, D15).
|
/// the list still in view above it ([QuickAddBar]; the developer's review,
|
||||||
/// What the title says is read as it is typed (`parseQuickAdd`): `#project`,
|
/// 3 Nov 2026); where sheets are dialogs (the desktop, expanded windows),
|
||||||
/// `@label`, `{deadline}`, `!!1`, `p1`, `!30m` (as many as typed), `!1pm`,
|
/// the same bar in a dialog. Its description opens from its + or the down
|
||||||
/// dates, times, durations and recurrences in the app's language and
|
/// arrow (D15). What the title says is read as it is typed
|
||||||
/// English, each highlighted and
|
/// (`parseQuickAdd`): `#project`, `@label`, `{deadline}`, `!!1`, `p1`,
|
||||||
/// shown on its chip; a tap on one keeps it as text, and a chip picked
|
/// `!30m` (as many as typed), `!1pm`, dates, times, durations and
|
||||||
|
/// recurrences in the app's language and English, each highlighted and
|
||||||
|
/// shown on its button; a tap on one keeps it as text, and a button's pick
|
||||||
/// takes its token out of the title. As `#` or `@` is typed, the projects
|
/// takes its token out of the title. As `#` or `@` is typed, the projects
|
||||||
/// or labels it may mean are offered below the title, one tapped
|
/// or labels it may mean are offered below the title, one tapped
|
||||||
/// completing it.
|
/// completing it.
|
||||||
|
|
@ -48,9 +49,8 @@ Future<void> showQuickAdd(
|
||||||
int? time,
|
int? time,
|
||||||
Duration? duration,
|
Duration? duration,
|
||||||
DropSpot? spot,
|
DropSpot? spot,
|
||||||
}) => showAdaptiveSheet<void>(
|
}) {
|
||||||
context,
|
final bar = _QuickAdd(
|
||||||
builder: (context) => _QuickAdd(
|
|
||||||
projectId: projectId ?? context.read<TodoCubit>().state.inboxId,
|
projectId: projectId ?? context.read<TodoCubit>().state.inboxId,
|
||||||
sectionId: sectionId,
|
sectionId: sectionId,
|
||||||
parentId: parentId,
|
parentId: parentId,
|
||||||
|
|
@ -59,8 +59,47 @@ Future<void> showQuickAdd(
|
||||||
null => null,
|
null => null,
|
||||||
},
|
},
|
||||||
spot: spot,
|
spot: spot,
|
||||||
),
|
);
|
||||||
);
|
if (sheetShowsAsDialog(context)) {
|
||||||
|
return showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Dialog(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 640),
|
||||||
|
child: bar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
useRootNavigator: true,
|
||||||
|
isScrollControlled: true,
|
||||||
|
useSafeArea: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
// Lighter than a sheet's: the list stays in view behind it.
|
||||||
|
barrierColor: Colors.black26,
|
||||||
|
builder: (context) => Padding(
|
||||||
|
padding: EdgeInsets.fromLTRB(
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
MediaQuery.viewInsetsOf(context).bottom + 8,
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(28)),
|
||||||
|
child: bar,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Quick add's bar, for tests to find.
|
||||||
|
typedef QuickAddBar = _QuickAdd;
|
||||||
|
|
||||||
/// The key of quick add's title field.
|
/// The key of quick add's title field.
|
||||||
const quickAddTitleKey = ValueKey('quick-add-title');
|
const quickAddTitleKey = ValueKey('quick-add-title');
|
||||||
|
|
@ -377,97 +416,114 @@ class _QuickAddState() extends State<_QuickAdd> {
|
||||||
};
|
};
|
||||||
final typing = _title.text.trim().isNotEmpty;
|
final typing = _title.text.trim().isNotEmpty;
|
||||||
final suggestions = _suggestions(l10n, state);
|
final suggestions = _suggestions(l10n, state);
|
||||||
return SheetScaffold(
|
final scheme = theme.colorScheme;
|
||||||
title: widget.parentId == null ? l10n.taskAddTitle : l10n.subTaskAdd,
|
const borderless = InputDecoration(
|
||||||
hasSnackBars: true,
|
border: InputBorder.none,
|
||||||
body: Column(
|
enabledBorder: InputBorder.none,
|
||||||
mainAxisSize: MainAxisSize.min,
|
focusedBorder: InputBorder.none,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
filled: false,
|
||||||
children: [
|
isDense: true,
|
||||||
// The down arrow opens the description, as Todoist's.
|
contentPadding: EdgeInsets.symmetric(vertical: 8),
|
||||||
CallbackShortcuts(
|
);
|
||||||
bindings: {
|
return Semantics(
|
||||||
const SingleActivator(LogicalKeyboardKey.arrowDown): _describe,
|
label: widget.parentId == null ? l10n.taskAddTitle : l10n.subTaskAdd,
|
||||||
},
|
container: true,
|
||||||
child: TextField(
|
child: Padding(
|
||||||
key: quickAddTitleKey,
|
padding: const EdgeInsets.fromLTRB(16, 8, 12, 12),
|
||||||
controller: _title,
|
child: Column(
|
||||||
focusNode: _focus,
|
mainAxisSize: MainAxisSize.min,
|
||||||
autofocus: true,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
textInputAction: TextInputAction.done,
|
children: [
|
||||||
textCapitalization: TextCapitalization.sentences,
|
Center(
|
||||||
decoration: InputDecoration(
|
child: Container(
|
||||||
hintText: l10n.taskTitleHint,
|
width: 32,
|
||||||
helperText: parsed.tokens.isEmpty
|
height: 4,
|
||||||
? null
|
decoration: BoxDecoration(
|
||||||
: l10n.quickAddTokenHint,
|
color: scheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||||
),
|
borderRadius: BorderRadius.circular(2),
|
||||||
onTap: _tapped,
|
),
|
||||||
onSubmitted: (_) => _add(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (suggestions.isNotEmpty)
|
|
||||||
Material(
|
|
||||||
key: quickAddSuggestionsKey,
|
|
||||||
color: theme.colorScheme.surfaceContainerHigh,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
for (final item in suggestions)
|
|
||||||
ListTile(
|
|
||||||
dense: true,
|
|
||||||
leading: Icon(switch (_typing()?.symbol) {
|
|
||||||
'@' => LucideIcons.tag,
|
|
||||||
_ when item.id == state.inboxId => LucideIcons.inbox,
|
|
||||||
_ => LucideIcons.hash,
|
|
||||||
}),
|
|
||||||
title: Text(item.name),
|
|
||||||
onTap: () => _complete(item),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_describing)
|
// The down arrow opens the description, as Todoist's.
|
||||||
TextField(
|
CallbackShortcuts(
|
||||||
controller: _description,
|
bindings: {
|
||||||
focusNode: _descriptionFocus,
|
const SingleActivator(LogicalKeyboardKey.arrowDown): _describe,
|
||||||
minLines: 1,
|
},
|
||||||
maxLines: 4,
|
child: TextField(
|
||||||
textCapitalization: TextCapitalization.sentences,
|
key: quickAddTitleKey,
|
||||||
decoration: InputDecoration(hintText: l10n.taskDescriptionHint),
|
controller: _title,
|
||||||
|
focusNode: _focus,
|
||||||
|
autofocus: true,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
style: theme.textTheme.titleLarge,
|
||||||
|
decoration: borderless.copyWith(
|
||||||
|
hintText: l10n.taskTitleHint,
|
||||||
|
helperText: parsed.tokens.isEmpty
|
||||||
|
? null
|
||||||
|
: l10n.quickAddTokenHint,
|
||||||
|
),
|
||||||
|
onTap: _tapped,
|
||||||
|
onSubmitted: (_) => _add(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
// The chips come once there is something to add.
|
if (suggestions.isNotEmpty)
|
||||||
AnimatedSize(
|
Material(
|
||||||
duration: const Duration(milliseconds: 150),
|
key: quickAddSuggestionsKey,
|
||||||
alignment: Alignment.topCenter,
|
color: scheme.surfaceContainerHighest,
|
||||||
child: !typing
|
borderRadius: BorderRadius.circular(12),
|
||||||
? const SizedBox(width: double.infinity)
|
clipBehavior: Clip.antiAlias,
|
||||||
: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.only(top: 12),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Wrap(
|
children: [
|
||||||
|
for (final item in suggestions)
|
||||||
|
ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: Icon(switch (_typing()?.symbol) {
|
||||||
|
'@' => LucideIcons.tag,
|
||||||
|
_ when item.id == state.inboxId => LucideIcons.inbox,
|
||||||
|
_ => LucideIcons.hash,
|
||||||
|
}),
|
||||||
|
title: Text(item.name),
|
||||||
|
onTap: () => _complete(item),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (_describing)
|
||||||
|
TextField(
|
||||||
|
controller: _description,
|
||||||
|
focusNode: _descriptionFocus,
|
||||||
|
minLines: 1,
|
||||||
|
maxLines: 4,
|
||||||
|
textCapitalization: TextCapitalization.sentences,
|
||||||
|
decoration: borderless.copyWith(
|
||||||
|
hintText: l10n.taskDescriptionHint,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 4,
|
|
||||||
children: [
|
children: [
|
||||||
ActionChip(
|
_more(context, deadline: deadline),
|
||||||
avatar: Icon(
|
QuickAddButton(
|
||||||
date?.repeat == null
|
icon: date?.repeat == null
|
||||||
? LucideIcons.calendar
|
? LucideIcons.calendar
|
||||||
: LucideIcons.repeat,
|
: LucideIcons.repeat,
|
||||||
color: switch (date) {
|
tooltip: l10n.dateTitle,
|
||||||
final date? => dateColor(
|
color: switch (date) {
|
||||||
context,
|
final date? => dateColor(context, date.day, today),
|
||||||
date.day,
|
null => null,
|
||||||
today,
|
},
|
||||||
),
|
label: switch (date) {
|
||||||
null => null,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
label: Text(switch (date) {
|
|
||||||
final date? => dateText(context, date, today),
|
final date? => dateText(context, date, today),
|
||||||
null => l10n.dateTitle,
|
null => null,
|
||||||
}),
|
},
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (await pickTaskDate(context, current: date)
|
if (await pickTaskDate(context, current: date)
|
||||||
case (:final date)) {
|
case (:final date)) {
|
||||||
|
|
@ -482,16 +538,22 @@ class _QuickAddState() extends State<_QuickAdd> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ActionChip(
|
if (deadline != null)
|
||||||
avatar: Icon(
|
QuickAddButton(
|
||||||
LucideIcons.flag,
|
icon: LucideIcons.target,
|
||||||
color: priorityColor(context, priority),
|
tooltip: l10n.deadlineTitle,
|
||||||
),
|
label: l10n.deadlineOn(
|
||||||
label: Text(
|
dayText(context, deadline, today),
|
||||||
priority == Priority.none
|
),
|
||||||
? l10n.priorityTitle
|
onPressed: () => _pickDeadline(deadline),
|
||||||
: priorityText(l10n, priority),
|
|
||||||
),
|
),
|
||||||
|
QuickAddButton(
|
||||||
|
icon: LucideIcons.flag,
|
||||||
|
tooltip: l10n.priorityTitle,
|
||||||
|
color: priorityColor(context, priority),
|
||||||
|
label: priority == Priority.none
|
||||||
|
? null
|
||||||
|
: priorityText(l10n, priority),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (await pickPriority(context, priority)
|
if (await pickPriority(context, priority)
|
||||||
case final picked?) {
|
case final picked?) {
|
||||||
|
|
@ -500,18 +562,31 @@ class _QuickAddState() extends State<_QuickAdd> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
QuickAddButton(
|
||||||
|
icon: LucideIcons.tag,
|
||||||
|
tooltip: l10n.labelsTitle,
|
||||||
|
label: labelCount == 0
|
||||||
|
? null
|
||||||
|
: l10n.labelsCount(labelCount),
|
||||||
|
onPressed: () async {
|
||||||
|
if (await pickLabels(context, [...labelIds])
|
||||||
|
case final labels?) {
|
||||||
|
_dropTokens(const {QuickAddKind.label});
|
||||||
|
setState(() => _labelIds = labels);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
if (widget.parentId == null)
|
if (widget.parentId == null)
|
||||||
ActionChip(
|
QuickAddButton(
|
||||||
avatar: Icon(
|
icon: project?.inbox ?? true
|
||||||
project?.inbox ?? true
|
? LucideIcons.inbox
|
||||||
? LucideIcons.inbox
|
: LucideIcons.hash,
|
||||||
: LucideIcons.hash,
|
tooltip: l10n.projectPickTitle,
|
||||||
),
|
label: switch (project) {
|
||||||
label: Text(switch (project) {
|
|
||||||
final project? when !project.inbox =>
|
final project? when !project.inbox =>
|
||||||
project.content!.name,
|
project.content!.name,
|
||||||
_ => l10n.shellTabInbox,
|
_ => l10n.shellTabInbox,
|
||||||
}),
|
},
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (await pickProject(context, projectId)
|
if (await pickProject(context, projectId)
|
||||||
case final id?) {
|
case final id?) {
|
||||||
|
|
@ -525,42 +600,20 @@ class _QuickAddState() extends State<_QuickAdd> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ActionChip(
|
|
||||||
avatar: const Icon(LucideIcons.tag),
|
|
||||||
label: Text(
|
|
||||||
labelCount == 0
|
|
||||||
? l10n.labelsTitle
|
|
||||||
: l10n.labelsCount(labelCount),
|
|
||||||
),
|
|
||||||
onPressed: () async {
|
|
||||||
if (await pickLabels(context, [...labelIds])
|
|
||||||
case final labels?) {
|
|
||||||
_dropTokens(const {QuickAddKind.label});
|
|
||||||
setState(() => _labelIds = labels);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (deadline != null)
|
|
||||||
ActionChip(
|
|
||||||
avatar: const Icon(LucideIcons.target),
|
|
||||||
label: Text(
|
|
||||||
l10n.deadlineOn(
|
|
||||||
dayText(context, deadline, today),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onPressed: () => _pickDeadline(deadline),
|
|
||||||
),
|
|
||||||
_more(context, deadline: deadline),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(width: 8),
|
||||||
),
|
IconButton.filled(
|
||||||
footer: FilledButton.icon(
|
tooltip: l10n.taskAdd,
|
||||||
icon: const Icon(LucideIcons.plus),
|
icon: const Icon(LucideIcons.arrowUp),
|
||||||
label: Text(l10n.taskAdd),
|
onPressed: typing && parsed.title.isNotEmpty ? _add : null,
|
||||||
onPressed: parsed.title.isEmpty ? null : _add,
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -592,6 +645,57 @@ class _QuickAddState() extends State<_QuickAdd> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
icon: const Icon(LucideIcons.plus),
|
icon: const Icon(LucideIcons.plus),
|
||||||
|
style: IconButton.styleFrom(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One of quick add's boxed buttons (the developer's review, 3 Nov 2026,
|
||||||
|
/// as Todoist's): its [icon] alone while nothing is set, named by
|
||||||
|
/// [tooltip]; with what is set as its [label] beside it.
|
||||||
|
class const QuickAddButton({
|
||||||
|
required final IconData icon,
|
||||||
|
required final String tooltip,
|
||||||
|
required final VoidCallback onPressed,
|
||||||
|
final String? label,
|
||||||
|
final Color? color,
|
||||||
|
super.key,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
|
final shown = color ?? scheme.onSurfaceVariant;
|
||||||
|
return Tooltip(
|
||||||
|
message: tooltip,
|
||||||
|
child: Material(
|
||||||
|
color: scheme.surfaceContainerHighest,
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: shown),
|
||||||
|
if (label case final label?) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -429,10 +429,10 @@ void main() {
|
||||||
await tester.enterText(find.byType(TextField).first, title);
|
await tester.enterText(find.byType(TextField).first, title);
|
||||||
// Add is on once there is a title.
|
// Add is on once there is a title.
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
// The sheet stays for the next one; its close button closes it.
|
// The bar stays for the next one; a tap beside it closes it.
|
||||||
await tester.tap(find.byTooltip('Close').last);
|
await tester.tapAt(const Offset(20, 20));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -707,7 +707,7 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.enterText(find.byKey(quickAddTitleKey), 'Envelopes');
|
await tester.enterText(find.byKey(quickAddTitleKey), 'Envelopes');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final all = await tester.runAsync(() => tasks().all());
|
final all = await tester.runAsync(() => tasks().all());
|
||||||
|
|
@ -959,7 +959,7 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.enterText(find.byKey(quickAddTitleKey), 'Call');
|
await tester.enterText(find.byKey(quickAddTitleKey), 'Call');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final stored = await tester.runAsync(() => tasks().all());
|
final stored = await tester.runAsync(() => tasks().all());
|
||||||
|
|
@ -1055,7 +1055,7 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.enterText(find.byKey(quickAddTitleKey), 'Call');
|
await tester.enterText(find.byKey(quickAddTitleKey), 'Call');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
final stored = await tester.runAsync(() => tasks().all());
|
final stored = await tester.runAsync(() => tasks().all());
|
||||||
|
|
@ -1245,28 +1245,52 @@ void main() {
|
||||||
Future<void> open(WidgetTester tester, String text) async {
|
Future<void> open(WidgetTester tester, String text) async {
|
||||||
await tester.tap(find.byTooltip('Add task'));
|
await tester.tap(find.byTooltip('Add task'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(
|
// Its buttons from the start, as Todoist's bar above the keyboard
|
||||||
find.widgetWithText(ActionChip, 'Priority'),
|
// (the developer's review, 3 Nov 2026).
|
||||||
findsNothing,
|
expect(find.byTooltip('Priority'), findsOneWidget);
|
||||||
reason: 'the title alone at first',
|
|
||||||
);
|
|
||||||
await tester.enterText(find.byKey(quickAddTitleKey), text);
|
await tester.enterText(find.byKey(quickAddTitleKey), text);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> add(WidgetTester tester) async {
|
Future<void> add(WidgetTester tester) async {
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
await goTab(tester, 'Inbox');
|
||||||
|
await seed(tester, TaskContent(title: 'Call mum', projectId: inbox()));
|
||||||
|
tester.view.viewInsets = const FakeViewPadding(bottom: 300);
|
||||||
|
addTearDown(tester.view.resetViewInsets);
|
||||||
|
|
||||||
|
await tester.tap(find.byTooltip('Add task'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final bar = tester.getRect(find.byType(QuickAddBar));
|
||||||
|
expect(bar.bottom, lessThanOrEqualTo(900 - 300), reason: 'above it');
|
||||||
|
expect(bar.bottom, greaterThan(900 - 300 - 24), reason: 'just above');
|
||||||
|
expect(bar.height, lessThan(200), reason: 'small');
|
||||||
|
expect(
|
||||||
|
tester.getRect(find.text('Call mum')).bottom,
|
||||||
|
lessThan(bar.top),
|
||||||
|
reason: 'the list in view',
|
||||||
|
);
|
||||||
|
expect(find.text('Add task'), findsNothing, reason: 'no title bar');
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('typed tokens set the task, shown on its chips, a label '
|
testWidgets('typed tokens set the task, shown on its chips, a label '
|
||||||
'unknown made', (tester) async {
|
'unknown made', (tester) async {
|
||||||
await start(tester, usedBefore: true);
|
await start(tester, usedBefore: true);
|
||||||
await goTab(tester, 'Inbox');
|
await goTab(tester, 'Inbox');
|
||||||
await open(tester, 'Buy milk tomorrow 6pm p1 @Garden');
|
await open(tester, 'Buy milk tomorrow 6pm p1 @Garden');
|
||||||
|
|
||||||
expect(find.widgetWithText(ActionChip, 'Priority 1'), findsOneWidget);
|
expect(
|
||||||
expect(find.widgetWithText(ActionChip, '1 label'), findsOneWidget);
|
find.widgetWithText(QuickAddButton, 'Priority 1'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.widgetWithText(QuickAddButton, '1 label'), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
find.text('Tap a highlighted word to keep it as text.'),
|
find.text('Tap a highlighted word to keep it as text.'),
|
||||||
findsOneWidget,
|
findsOneWidget,
|
||||||
|
|
@ -1389,7 +1413,7 @@ void main() {
|
||||||
await goTab(tester, 'Inbox');
|
await goTab(tester, 'Inbox');
|
||||||
await open(tester, 'Pay tomorrow');
|
await open(tester, 'Pay tomorrow');
|
||||||
|
|
||||||
await tester.tap(find.widgetWithText(ActionChip, 'Tomorrow'));
|
await tester.tap(find.widgetWithText(QuickAddButton, 'Tomorrow'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.widgetWithText(ListTile, 'Today').last);
|
await tester.tap(find.widgetWithText(ListTile, 'Today').last);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
@ -1400,7 +1424,7 @@ void main() {
|
||||||
?.text,
|
?.text,
|
||||||
'Pay',
|
'Pay',
|
||||||
);
|
);
|
||||||
expect(find.widgetWithText(ActionChip, 'Today'), findsOneWidget);
|
expect(find.widgetWithText(QuickAddButton, 'Today'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.byKey(quickAddTitleKey));
|
await tester.tap(find.byKey(quickAddTitleKey));
|
||||||
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
await tester.sendKeyEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
|
@ -1874,9 +1898,9 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.enterText(find.byType(TextField).last, 'Socks');
|
await tester.enterText(find.byType(TextField).last, 'Socks');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byTooltip('Close').last);
|
await tester.tapAt(const Offset(20, 20));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Sub-tasks 0/1'), findsOneWidget);
|
expect(find.text('Sub-tasks 0/1'), findsOneWidget);
|
||||||
|
|
||||||
|
|
@ -2402,9 +2426,9 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.enterText(find.byKey(quickAddTitleKey), 'Between');
|
await tester.enterText(find.byKey(quickAddTitleKey), 'Between');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
await tester.tap(find.byTooltip('Close').last);
|
await tester.tapAt(const Offset(20, 20));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(order(tester, ['Alpha', 'Bravo', 'Between']), [
|
expect(order(tester, ['Alpha', 'Bravo', 'Between']), [
|
||||||
'Alpha',
|
'Alpha',
|
||||||
|
|
@ -2477,7 +2501,7 @@ void main() {
|
||||||
);
|
);
|
||||||
await tester.enterText(find.byKey(quickAddTitleKey), 'Later');
|
await tester.enterText(find.byKey(quickAddTitleKey), 'Later');
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
await tester.tap(find.widgetWithText(FilledButton, 'Add'));
|
await tester.tap(find.byTooltip('Add'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
expect((await stored(tester))['Later']!.date?.day, tomorrow);
|
expect((await stored(tester))['Later']!.date?.day, tomorrow);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue