feat(todo): a task's sheet partway up and its parts in rounded boxes, as Todoist's
The developer's review of 3 Nov 2026 (ADR 0033, D24): on a phone the sheet opens as high as its content, at most 65 % of the window, the list above it in view, and is dragged up to the whole window or down to close; its title, description, date and reminders each in a rounded box, its priority, labels, deadline and project as a row of chips (named "Priority", "Labels", "Deadline" while unset), and its sub-tasks in a box ending in Add sub-task. Where sheets are dialogs, and in the pane beside the list, the same parts. Todo keeps no comments, so the sheet has no comment field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
8438caa489
commit
140a283d0e
2 changed files with 401 additions and 181 deletions
|
|
@ -40,12 +40,111 @@ Future<void> openTaskSheet(BuildContext context, String id) async {
|
|||
return;
|
||||
}
|
||||
}
|
||||
await showAdaptiveSheet<void>(
|
||||
context,
|
||||
builder: (context) => _TaskSheetHost(id: id),
|
||||
if (sheetShowsAsDialog(context)) {
|
||||
await showAdaptiveSheet<void>(
|
||||
context,
|
||||
builder: (context) => _TaskSheetHost(id: id),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// On a phone partway up, as high as its content (at most 65 % of the
|
||||
// window), and dragged up to the whole window, as Todoist's (the
|
||||
// developer's review, 3 Nov 2026).
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
builder: (context) => _DraggableSheet(
|
||||
builder: (scroll) => _SheetScroll(
|
||||
controller: scroll,
|
||||
child: _TaskSheetHost(id: id),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The key of the handle atop a task's sheet on a phone.
|
||||
const taskSheetHandleKey = ValueKey('task-sheet-handle');
|
||||
|
||||
/// A task's sheet on a phone: partway up as high as its content, at most
|
||||
/// [_partway] of the window, and up to the whole window dragged.
|
||||
class const _DraggableSheet({
|
||||
required final Widget Function(ScrollController scroll) builder,
|
||||
}) extends StatefulWidget {
|
||||
/// How far up it opens at most.
|
||||
static const _partway = 0.65;
|
||||
|
||||
@override
|
||||
State<_DraggableSheet> createState() => _DraggableSheetState();
|
||||
}
|
||||
|
||||
class _DraggableSheetState() extends State<_DraggableSheet> {
|
||||
final _sheet = DraggableScrollableController();
|
||||
ScrollController? _scroll;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Once laid out, down to its content's height where that is less.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final scroll = _scroll;
|
||||
if (!mounted || scroll == null || !scroll.hasClients) {
|
||||
return;
|
||||
}
|
||||
final position = scroll.position;
|
||||
final window = MediaQuery.sizeOf(context).height;
|
||||
final content = position.maxScrollExtent + position.viewportDimension;
|
||||
final size = (content / window).clamp(0.25, _DraggableSheet._partway);
|
||||
if (_sheet.isAttached && size < _sheet.size) {
|
||||
_sheet.jumpTo(size);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sheet.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DraggableScrollableSheet(
|
||||
controller: _sheet,
|
||||
expand: false,
|
||||
initialChildSize: _DraggableSheet._partway,
|
||||
minChildSize: 0.2,
|
||||
snap: true,
|
||||
builder: (context, scroll) {
|
||||
_scroll = scroll;
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
),
|
||||
child: widget.builder(scroll),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// The scrolling of a [_DraggableSheet] for the task's sheet inside it,
|
||||
/// whose whole content scrolls with it so that dragging it drags the sheet.
|
||||
class const _SheetScroll({
|
||||
required final ScrollController controller,
|
||||
required super.child,
|
||||
}) extends InheritedWidget {
|
||||
static ScrollController? maybeOf(BuildContext context) =>
|
||||
context.dependOnInheritedWidgetOfExactType<_SheetScroll>()?.controller;
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(_SheetScroll oldWidget) =>
|
||||
controller != oldWidget.controller;
|
||||
}
|
||||
|
||||
/// A sheet's task, which a step of its path replaces in place.
|
||||
class const _TaskSheetHost({required final String id}) extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -239,6 +338,47 @@ class _TaskSheetState() extends State<_TaskSheet> {
|
|||
Widget? heading,
|
||||
Widget? action,
|
||||
}) {
|
||||
if (_SheetScroll.maybeOf(context) case final scroll? when !widget.inPane) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: ListView(
|
||||
controller: scroll,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 24),
|
||||
children: [
|
||||
Center(
|
||||
key: taskSheetHandleKey,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 12),
|
||||
width: 32,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child:
|
||||
heading ??
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
?action,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
body,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!widget.inPane) {
|
||||
return SheetScaffold(
|
||||
title: title,
|
||||
|
|
@ -421,214 +561,271 @@ class _TaskSheetState() extends State<_TaskSheet> {
|
|||
},
|
||||
),
|
||||
action: _menu(context, task, subTasks),
|
||||
// Its parts in rounded boxes, as Todoist's (the developer's review,
|
||||
// 3 Nov 2026): the title, the description, the date, the reminders,
|
||||
// then the priority, labels, deadline and project as chips, and the
|
||||
// sub-tasks with one more to add.
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
TaskCheck(task: task),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _title,
|
||||
style: theme.textTheme.titleLarge,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: l10n.taskTitleHint,
|
||||
_Box(
|
||||
child: Row(
|
||||
children: [
|
||||
TaskCheck(task: task),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _title,
|
||||
style: theme.textTheme.titleLarge,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
filled: false,
|
||||
hintText: l10n.taskTitleHint,
|
||||
),
|
||||
onSubmitted: (_) => _saveText(),
|
||||
onTapOutside: (_) => unawaited(_saveText()),
|
||||
),
|
||||
onSubmitted: (_) => _saveText(),
|
||||
onTapOutside: (_) => unawaited(_saveText()),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
// The icon stays by the first line however long the
|
||||
// description grows (the developer's, 1 Nov).
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||
child: Icon(LucideIcons.text),
|
||||
),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _description,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: l10n.taskDescriptionHint,
|
||||
),
|
||||
onTapOutside: (_) => unawaited(_saveText()),
|
||||
_Box(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(12, 12, 12, 0),
|
||||
child: Icon(LucideIcons.text),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
LucideIcons.calendar,
|
||||
color: switch (content.date) {
|
||||
final date? => dateColor(context, date.day, today),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
title: Text(switch (content.date) {
|
||||
final date? => dateText(context, date, today),
|
||||
null => l10n.dateNoneYet,
|
||||
}),
|
||||
subtitle: switch (content.date) {
|
||||
TaskDate(:final day, repeat: final repeat?) => Text(
|
||||
repeatText(context, repeat, day),
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
trailing: content.date == null
|
||||
? null
|
||||
: clear(() => _change((c) => c.copyWith(date: () => null))),
|
||||
onTap: () async {
|
||||
if (await pickTaskDate(context, current: content.date) case (
|
||||
:final date,
|
||||
)) {
|
||||
await _change((c) => c.copyWith(date: () => date));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.target),
|
||||
title: Text(switch (content.deadline) {
|
||||
final day? => l10n.deadlineOn(dayText(context, day, today)),
|
||||
null => l10n.deadlineNone,
|
||||
}),
|
||||
trailing: content.deadline == null
|
||||
? null
|
||||
: clear(() => _change((c) => c.copyWith(deadline: () => null))),
|
||||
onTap: () async {
|
||||
if (await pickDeadline(context, current: content.deadline) case (
|
||||
:final day,
|
||||
)) {
|
||||
await _change((c) => c.copyWith(deadline: () => day));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.bell),
|
||||
title: Text(remindersText(context, content, today)),
|
||||
onTap: () async {
|
||||
final timed = content.date?.time != null;
|
||||
final chosen = await pickReminders(
|
||||
context,
|
||||
before: content.reminders,
|
||||
at: content.reminderTimes,
|
||||
timed: timed,
|
||||
day: content.date?.day,
|
||||
);
|
||||
if (chosen == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
// Asked once there is something to remind of, not before.
|
||||
final had =
|
||||
(timed && content.reminders.isNotEmpty) ||
|
||||
content.reminderTimes.isNotEmpty;
|
||||
if (!had && (chosen.before.isNotEmpty || chosen.at.isNotEmpty)) {
|
||||
final reminders = PersonScope.of<TodoPerson>(
|
||||
context,
|
||||
).reminders;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (!await reminders.askPermission()) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.remindersNotAllowed)),
|
||||
);
|
||||
}
|
||||
}
|
||||
await _change(
|
||||
(c) => c.copyWith(
|
||||
reminders: chosen.before,
|
||||
reminderTimes: chosen.at,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
LucideIcons.flag,
|
||||
color: priorityColor(context, content.priority),
|
||||
),
|
||||
title: Text(priorityText(l10n, content.priority)),
|
||||
onTap: () async {
|
||||
if (await pickPriority(context, content.priority)
|
||||
case final priority?) {
|
||||
await _change((c) => c.copyWith(priority: priority));
|
||||
}
|
||||
},
|
||||
),
|
||||
if (content.parentId == null)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
project?.inbox ?? true ? LucideIcons.inbox : LucideIcons.hash,
|
||||
),
|
||||
title: Text(switch (project) {
|
||||
final project? when !project.inbox => project.content!.name,
|
||||
_ => l10n.shellTabInbox,
|
||||
}),
|
||||
onTap: () async {
|
||||
if (await pickProject(context, content.projectId) case final id?
|
||||
when id != content.projectId) {
|
||||
await _move(id);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.tag),
|
||||
title: Text(
|
||||
content.labelIds.isEmpty
|
||||
? l10n.labelsNoneSet
|
||||
: [
|
||||
for (final id in content.labelIds) ?labels[id],
|
||||
].join(', '),
|
||||
),
|
||||
onTap: () async {
|
||||
if (await pickLabels(context, content.labelIds)
|
||||
case final chosen?) {
|
||||
await _change((c) => c.copyWith(labelIds: chosen));
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(
|
||||
subTasks.isEmpty
|
||||
? l10n.subTasks
|
||||
: l10n.subTasksDone(
|
||||
subTasks.where((t) => t.content.completed).length,
|
||||
subTasks.length,
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _description,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
filled: false,
|
||||
hintText: l10n.taskDescriptionHint,
|
||||
),
|
||||
style: theme.textTheme.titleSmall?.copyWith(color: muted),
|
||||
onTapOutside: (_) => unawaited(_saveText()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
for (final sub in subTasks)
|
||||
if (!(content.hideDoneSubTasks && sub.content.completed))
|
||||
TaskTile(task: sub, selectable: false),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(l10n.subTaskAdd),
|
||||
onPressed: () => showQuickAdd(
|
||||
context,
|
||||
projectId: content.projectId,
|
||||
sectionId: content.sectionId,
|
||||
parentId: task.id,
|
||||
_Box(
|
||||
child: ListTile(
|
||||
leading: Icon(
|
||||
LucideIcons.calendar,
|
||||
color: switch (content.date) {
|
||||
final date? => dateColor(context, date.day, today),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
title: Text(switch (content.date) {
|
||||
final date? => dateText(context, date, today),
|
||||
null => l10n.dateNoneYet,
|
||||
}),
|
||||
subtitle: switch (content.date) {
|
||||
TaskDate(:final day, repeat: final repeat?) => Text(
|
||||
repeatText(context, repeat, day),
|
||||
),
|
||||
_ => null,
|
||||
},
|
||||
trailing: content.date == null
|
||||
? null
|
||||
: clear(() => _change((c) => c.copyWith(date: () => null))),
|
||||
onTap: () async {
|
||||
if (await pickTaskDate(context, current: content.date) case (
|
||||
:final date,
|
||||
)) {
|
||||
await _change((c) => c.copyWith(date: () => date));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
_Box(
|
||||
child: ListTile(
|
||||
leading: const Icon(LucideIcons.bell),
|
||||
title: Text(remindersText(context, content, today)),
|
||||
onTap: () => _pickReminders(context, content),
|
||||
),
|
||||
),
|
||||
_Box(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
LucideIcons.flag,
|
||||
color: priorityColor(context, content.priority),
|
||||
),
|
||||
label: Text(
|
||||
content.priority == Priority.none
|
||||
? l10n.priorityTitle
|
||||
: priorityText(l10n, content.priority),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (await pickPriority(context, content.priority)
|
||||
case final priority?) {
|
||||
await _change((c) => c.copyWith(priority: priority));
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(LucideIcons.tag),
|
||||
label: Text(
|
||||
content.labelIds.isEmpty
|
||||
? l10n.labelsTitle
|
||||
: [
|
||||
for (final id in content.labelIds) ?labels[id],
|
||||
].join(', '),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (await pickLabels(context, content.labelIds)
|
||||
case final chosen?) {
|
||||
await _change((c) => c.copyWith(labelIds: chosen));
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(LucideIcons.target),
|
||||
label: Text(switch (content.deadline) {
|
||||
final day? => l10n.deadlineOn(
|
||||
dayText(context, day, today),
|
||||
),
|
||||
null => l10n.deadlineTitle,
|
||||
}),
|
||||
onPressed: () async {
|
||||
if (await pickDeadline(
|
||||
context,
|
||||
current: content.deadline,
|
||||
)
|
||||
case (:final day)) {
|
||||
await _change((c) => c.copyWith(deadline: () => day));
|
||||
}
|
||||
},
|
||||
),
|
||||
if (content.parentId == null)
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
project?.inbox ?? true
|
||||
? LucideIcons.inbox
|
||||
: LucideIcons.hash,
|
||||
),
|
||||
label: Text(switch (project) {
|
||||
final project? when !project.inbox =>
|
||||
project.content!.name,
|
||||
_ => l10n.shellTabInbox,
|
||||
}),
|
||||
onPressed: () async {
|
||||
if (await pickProject(context, content.projectId)
|
||||
case final id? when id != content.projectId) {
|
||||
await _move(id);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_Box(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (subTasks.isNotEmpty) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(
|
||||
l10n.subTasksDone(
|
||||
subTasks.where((t) => t.content.completed).length,
|
||||
subTasks.length,
|
||||
),
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: muted,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final sub in subTasks)
|
||||
if (!(content.hideDoneSubTasks && sub.content.completed))
|
||||
TaskTile(task: sub, selectable: false),
|
||||
],
|
||||
TextButton.icon(
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(l10n.subTaskAdd),
|
||||
onPressed: () => showQuickAdd(
|
||||
context,
|
||||
projectId: content.projectId,
|
||||
sectionId: content.sectionId,
|
||||
parentId: task.id,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Asks for the task's reminders and writes them, asking first to
|
||||
/// remind where nothing was to be reminded of before.
|
||||
Future<void> _pickReminders(BuildContext context, TaskContent content) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final timed = content.date?.time != null;
|
||||
final chosen = await pickReminders(
|
||||
context,
|
||||
before: content.reminders,
|
||||
at: content.reminderTimes,
|
||||
timed: timed,
|
||||
day: content.date?.day,
|
||||
);
|
||||
if (chosen == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
// Asked once there is something to remind of, not before.
|
||||
final had =
|
||||
(timed && content.reminders.isNotEmpty) ||
|
||||
content.reminderTimes.isNotEmpty;
|
||||
if (!had && (chosen.before.isNotEmpty || chosen.at.isNotEmpty)) {
|
||||
final reminders = PersonScope.of<TodoPerson>(context).reminders;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
if (!await reminders.askPermission()) {
|
||||
messenger.showSnackBar(
|
||||
SnackBar(content: Text(l10n.remindersNotAllowed)),
|
||||
);
|
||||
}
|
||||
}
|
||||
await _change(
|
||||
(c) => c.copyWith(reminders: chosen.before, reminderTimes: chosen.at),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// One part of a task's sheet in its rounded box (the developer's review,
|
||||
/// 3 Nov 2026, as Todoist's).
|
||||
class const _Box({required final Widget child}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Material(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The key of the path atop a task's sheet, for tests to find it by.
|
||||
|
|
|
|||
|
|
@ -1877,6 +1877,29 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
testWidgets("a task's sheet opens partway up, as Todoist's, its parts in "
|
||||
'rounded boxes and its priority, labels and deadline as chips; '
|
||||
"dragged up, it fills the window (the developer's review, 3 Nov "
|
||||
'2026)', (tester) async {
|
||||
await start(tester, usedBefore: true);
|
||||
await seed(tester, TaskContent(title: 'Pack', projectId: inbox()));
|
||||
await goTab(tester, 'Inbox');
|
||||
|
||||
await tester.tap(find.text('Pack'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final sheet = find.byType(DraggableScrollableSheet);
|
||||
expect(tester.getRect(sheet).top, greaterThan(900 * 0.3));
|
||||
for (final chip in ['Priority', 'Labels', 'Deadline']) {
|
||||
expect(find.widgetWithText(ActionChip, chip), findsOneWidget);
|
||||
}
|
||||
expect(find.text('Add sub-task'), findsOneWidget);
|
||||
|
||||
await tester.drag(find.byKey(taskSheetHandleKey), const Offset(0, -700));
|
||||
await tester.pumpAndSettle();
|
||||
expect(tester.getRect(sheet).top, lessThan(80));
|
||||
});
|
||||
|
||||
testWidgets("a task's sheet sets its priority, adds a sub-task the row "
|
||||
'counts, and deletes it with its sub-tasks after asking', (
|
||||
tester,
|
||||
|
|
@ -1888,7 +1911,7 @@ void main() {
|
|||
|
||||
await tester.tap(find.text('Pack'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('No priority'));
|
||||
await tester.tap(find.widgetWithText(ActionChip, 'Priority'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Priority 1'));
|
||||
await tester.pumpAndSettle();
|
||||
|
|
|
|||
Loading…
Reference in a new issue