fix(ui): pin edit-sheet Save button as a fixed footer
The variety edit sheet grew with v8 fields (organic, needs-reproduction, crop calendar) and its Save button, sitting at the bottom of the SingleChildScrollView, dropped below the fold — the species-linking widget test could not reach it (tap landed off-screen, species never linked). Move the fields into a Flexible scroll area and pin the Cancel/Save row as a fixed footer so Save is always visible and tappable. Full suite green (233 tests).
This commit is contained in:
parent
733c118c3b
commit
0e6ef13d00
1 changed files with 148 additions and 135 deletions
|
|
@ -667,145 +667,158 @@ class _EditVarietySheetState extends State<_EditVarietySheet> {
|
|||
top: 16,
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom + 16,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.editVariety.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.name'),
|
||||
controller: _name,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.name,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
// Fields scroll; the Cancel/Save row is pinned as a fixed footer so Save
|
||||
// stays visible and tappable no matter how tall the sheet grows.
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.editVariety.title,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.name'),
|
||||
controller: _name,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.name,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.species'),
|
||||
controller: _speciesField,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.species,
|
||||
hintText: t.editVariety.speciesHint,
|
||||
prefixIcon: const Icon(Icons.eco_outlined),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
onChanged: _onSpeciesQuery,
|
||||
),
|
||||
for (final match in _suggestions)
|
||||
ListTile(
|
||||
dense: true,
|
||||
key: Key('species.option.${match.id}'),
|
||||
title: Text(match.displayLabel),
|
||||
subtitle: match.family == null
|
||||
? null
|
||||
: Text(match.family!),
|
||||
onTap: () => _pick(match),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _category,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.category,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notes,
|
||||
minLines: 2,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.notes,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SwitchListTile(
|
||||
key: const Key('editVariety.organic'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
secondary: const Icon(Icons.eco_outlined, color: seedGreen),
|
||||
title: Text(t.editVariety.organic),
|
||||
subtitle: Text(t.editVariety.organicHint),
|
||||
value: _isOrganic,
|
||||
onChanged: (v) => setState(() => _isOrganic = v),
|
||||
),
|
||||
SwitchListTile(
|
||||
key: const Key('editVariety.needsReproduction'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
secondary: const Icon(Icons.autorenew, color: seedGreen),
|
||||
title: Text(t.needsReproduction.label),
|
||||
subtitle: Text(t.needsReproduction.hint),
|
||||
value: _needsReproduction,
|
||||
onChanged: (v) => setState(() => _needsReproduction = v),
|
||||
),
|
||||
// Crop calendar: one collapsed reveal, not five dropdowns up front.
|
||||
ExpansionTile(
|
||||
key: const Key('editVariety.cropCalendar'),
|
||||
initiallyExpanded: _calendarOpen,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: const EdgeInsets.only(bottom: 8),
|
||||
leading: const Icon(Icons.calendar_month_outlined),
|
||||
title: Text(t.cropCalendar.title),
|
||||
onExpansionChanged: (v) => _calendarOpen = v,
|
||||
children: [
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.sow,
|
||||
mask: _sowMonths,
|
||||
onChanged: (m) => setState(() => _sowMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.transplant,
|
||||
mask: _transplantMonths,
|
||||
onChanged: (m) => setState(() => _transplantMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.flowering,
|
||||
mask: _floweringMonths,
|
||||
onChanged: (m) => setState(() => _floweringMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.fruiting,
|
||||
mask: _fruitingMonths,
|
||||
onChanged: (m) => setState(() => _fruitingMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.seedHarvest,
|
||||
mask: _seedHarvestMonths,
|
||||
onChanged: (m) =>
|
||||
setState(() => _seedHarvestMonths = m),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
key: const Key('editVariety.species'),
|
||||
controller: _speciesField,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.species,
|
||||
hintText: t.editVariety.speciesHint,
|
||||
prefixIcon: const Icon(Icons.eco_outlined),
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
onChanged: _onSpeciesQuery,
|
||||
),
|
||||
for (final match in _suggestions)
|
||||
ListTile(
|
||||
dense: true,
|
||||
key: Key('species.option.${match.id}'),
|
||||
title: Text(match.displayLabel),
|
||||
subtitle: match.family == null ? null : Text(match.family!),
|
||||
onTap: () => _pick(match),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('editVariety.save'),
|
||||
onPressed: _save,
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _category,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.category,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _notes,
|
||||
minLines: 2,
|
||||
maxLines: 5,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.editVariety.notes,
|
||||
border: const OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SwitchListTile(
|
||||
key: const Key('editVariety.organic'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
secondary: const Icon(Icons.eco_outlined, color: seedGreen),
|
||||
title: Text(t.editVariety.organic),
|
||||
subtitle: Text(t.editVariety.organicHint),
|
||||
value: _isOrganic,
|
||||
onChanged: (v) => setState(() => _isOrganic = v),
|
||||
),
|
||||
SwitchListTile(
|
||||
key: const Key('editVariety.needsReproduction'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
secondary: const Icon(Icons.autorenew, color: seedGreen),
|
||||
title: Text(t.needsReproduction.label),
|
||||
subtitle: Text(t.needsReproduction.hint),
|
||||
value: _needsReproduction,
|
||||
onChanged: (v) => setState(() => _needsReproduction = v),
|
||||
),
|
||||
// Crop calendar: one collapsed reveal, not five dropdowns up front.
|
||||
ExpansionTile(
|
||||
key: const Key('editVariety.cropCalendar'),
|
||||
initiallyExpanded: _calendarOpen,
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: const EdgeInsets.only(bottom: 8),
|
||||
leading: const Icon(Icons.calendar_month_outlined),
|
||||
title: Text(t.cropCalendar.title),
|
||||
onExpansionChanged: (v) => _calendarOpen = v,
|
||||
children: [
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.sow,
|
||||
mask: _sowMonths,
|
||||
onChanged: (m) => setState(() => _sowMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.transplant,
|
||||
mask: _transplantMonths,
|
||||
onChanged: (m) => setState(() => _transplantMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.flowering,
|
||||
mask: _floweringMonths,
|
||||
onChanged: (m) => setState(() => _floweringMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.fruiting,
|
||||
mask: _fruitingMonths,
|
||||
onChanged: (m) => setState(() => _fruitingMonths = m),
|
||||
),
|
||||
_MonthMultiSelect(
|
||||
label: t.cropCalendar.seedHarvest,
|
||||
mask: _seedHarvestMonths,
|
||||
onChanged: (m) => setState(() => _seedHarvestMonths = m),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(t.common.cancel),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(
|
||||
key: const Key('editVariety.save'),
|
||||
onPressed: _save,
|
||||
child: Text(t.common.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue