Skip to content

Commit

Permalink
fix: AutocompleteWidget: Scrollbar + dividers + correct width (#2704)
Browse files Browse the repository at this point in the history
* AutocompleteWidget: Scrollbar + dividers + correct width
* Split the item in the autocompleteOption widget to a dedicated widget
  • Loading branch information
g123k authored Aug 1, 2022
1 parent 89ffd1e commit 1618781
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 106 deletions.
99 changes: 69 additions & 30 deletions packages/smooth_app/lib/pages/product/autocomplete.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,51 +14,90 @@ class AutocompleteOptions<T extends Object> extends StatelessWidget {
required this.onSelected,
required this.options,
required this.maxOptionsHeight,
}) : super(key: key);
required this.maxOptionsWidth,
}) : assert(maxOptionsHeight >= 0),
assert(maxOptionsWidth >= 0),
super(key: key);

final AutocompleteOptionToString<T> displayStringForOption;

final AutocompleteOnSelected<T> onSelected;

final Iterable<T> options;
final double maxOptionsWidth;
final double maxOptionsHeight;

@override
Widget build(BuildContext context) {
final int highlightedOption = AutocompleteHighlightedOption.of(context);

return Align(
alignment: AlignmentDirectional.topStart,
child: Material(
elevation: 4.0,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxOptionsHeight),
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final T option = options.elementAt(index);
return InkWell(
onTap: () {
onSelected(option);
},
child: Builder(builder: (BuildContext context) {
final bool highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5);
});
}
return Container(
color: highlight ? Theme.of(context).focusColor : null,
padding: const EdgeInsets.all(LARGE_SPACE),
child: Text(displayStringForOption(option)),
);
}),
);
},
constraints: BoxConstraints(
maxHeight: maxOptionsHeight,
maxWidth: maxOptionsWidth,
minWidth: 100.0,
),
child: Scrollbar(
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final T option = options.elementAt(index);

return _AutocompleteOptionsItem<T>(
key: Key(index.toString()),
option: option,
highlight: highlightedOption == index,
onSelected: onSelected,
displayStringForOption: displayStringForOption,
);
},
separatorBuilder: (_, __) => const Divider(
height: 1.0,
),
),
),
),
),
);
}
}

class _AutocompleteOptionsItem<T extends Object> extends StatelessWidget {
const _AutocompleteOptionsItem({
required this.option,
required this.highlight,
required this.displayStringForOption,
required this.onSelected,
Key? key,
}) : super(key: key);

final T option;
final bool highlight;
final AutocompleteOptionToString<T> displayStringForOption;
final AutocompleteOnSelected<T> onSelected;

@override
Widget build(BuildContext context) {
if (highlight) {
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(context, alignment: 0.5);
});
}

return InkWell(
onTap: () {
onSelected(option);
},
child: Container(
color: highlight ? Theme.of(context).focusColor : null,
padding: const EdgeInsets.all(LARGE_SPACE),
child: Text(
displayStringForOption(option),
),
),
);
Expand Down
162 changes: 86 additions & 76 deletions packages/smooth_app/lib/pages/product/simple_input_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,87 +50,97 @@ class _SimpleInputWidgetState extends State<SimpleInputWidget> {
),
),
ExplanationWidget(widget.helper.getAddExplanations(appLocalizations)),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: LARGE_SPACE),
child: RawAutocomplete<String>(
key: _autocompleteKey,
focusNode: _focusNode,
textEditingController: widget.controller,
optionsBuilder: (final TextEditingValue value) async {
final List<String> result = <String>[];
final String input = value.text.trim();
if (input.isEmpty) {
return result;
}
final TagType? tagType = widget.helper.getTagType();
if (tagType == null) {
return result;
}
// TODO(monsieurtanuki): ask off-dart to return Strings instead of dynamic?
final List<dynamic> data =
await OpenFoodAPIClient.getAutocompletedSuggestions(
tagType,
language: ProductQuery.getLanguage()!,
limit: 1000000, // lower max count on the server anyway
input: value.text.trim(),
);
for (final dynamic item in data) {
result.add(item.toString());
}
result.sort();
return result;
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) =>
TextField(
controller: textEditingController,
decoration: InputDecoration(
filled: true,
border: const OutlineInputBorder(
borderRadius: CIRCULAR_BORDER_RADIUS,
borderSide: BorderSide.none,
LayoutBuilder(
builder: (_, BoxConstraints constraints) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Flexible(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: LARGE_SPACE),
child: RawAutocomplete<String>(
key: _autocompleteKey,
focusNode: _focusNode,
textEditingController: widget.controller,
optionsBuilder: (final TextEditingValue value) async {
final List<String> result = <String>[];
final String input = value.text.trim();
if (input.isEmpty) {
return result;
}
final TagType? tagType = widget.helper.getTagType();
if (tagType == null) {
return result;
}
// TODO(monsieurtanuki): ask off-dart to return Strings instead of dynamic?
final List<dynamic> data =
await OpenFoodAPIClient.getAutocompletedSuggestions(
tagType,
language: ProductQuery.getLanguage()!,
limit:
1000000, // lower max count on the server anyway
input: value.text.trim(),
);
for (final dynamic item in data) {
result.add(item.toString());
}
result.sort();
return result;
},
fieldViewBuilder: (BuildContext context,
TextEditingController textEditingController,
FocusNode focusNode,
VoidCallback onFieldSubmitted) =>
TextField(
controller: textEditingController,
decoration: InputDecoration(
filled: true,
border: const OutlineInputBorder(
borderRadius: CIRCULAR_BORDER_RADIUS,
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: SMALL_SPACE,
vertical: SMALL_SPACE,
),
hintText: widget.helper.getAddHint(appLocalizations),
),
autofocus: true,
focusNode: focusNode,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: SMALL_SPACE,
vertical: SMALL_SPACE,
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) =>
AutocompleteOptions<String>(
displayStringForOption:
RawAutocomplete.defaultStringForOption,
onSelected: onSelected,
options: options,
// Width = Row width - horizontal padding
maxOptionsWidth:
constraints.maxWidth - (LARGE_SPACE * 2),
maxOptionsHeight:
MediaQuery.of(context).size.height / 2,
),
hintText: widget.helper.getAddHint(appLocalizations),
),
autofocus: true,
focusNode: focusNode,
),
optionsViewBuilder: (
BuildContext context,
AutocompleteOnSelected<String> onSelected,
Iterable<String> options,
) =>
AutocompleteOptions<String>(
displayStringForOption:
RawAutocomplete.defaultStringForOption,
onSelected: onSelected,
options: options,
maxOptionsHeight: MediaQuery.of(context).size.height / 2,
),
),
),
),
IconButton(
onPressed: () {
if (widget.helper.addItemsFromController(widget.controller)) {
setState(() {});
}
},
icon: const Icon(Icons.add_circle),
),
],
IconButton(
onPressed: () {
if (widget.helper
.addItemsFromController(widget.controller)) {
setState(() {});
}
},
icon: const Icon(Icons.add_circle),
),
],
);
},
),
Divider(color: themeData.colorScheme.onBackground),
Column(
Expand Down

0 comments on commit 1618781

Please sign in to comment.