From 23e8c02b506e426bbc52ab75c25ff4e052e2ec42 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 15 Jan 2025 02:12:03 +0700 Subject: [PATCH 1/5] Fix the issue where the `suggestionsBoxMaxHeight` property is not working correctly --- example/lib/main.dart | 23 ++++++++++++++++++++++- lib/tag_editor.dart | 16 +++++++++------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index a2809ad..888d5cb 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -34,7 +34,27 @@ class _MyHomePageState extends State { 'dat@gmail.com', 'dab246@gmail.com', 'kaka@gmail.com', - 'datvu@gmail.com' + 'datvu@gmail.com', + 'datvu1@gmail.com', + 'datvu2@gmail.com', + 'datvu3@gmail.com', + 'datvu4@gmail.com', + 'datvu5@gmail.com', + 'datvu6@gmail.com', + 'datvu7@gmail.com', + 'datvu8@gmail.com', + 'datvu9@gmail.com', + 'datvu10@gmail.com', + 'datvu11@gmail.com', + 'datvu12@gmail.com', + 'datvu13@gmail.com', + 'datvu14@gmail.com', + 'datvu15@gmail.com', + 'datvu16@gmail.com', + 'datvu17@gmail.com', + 'datvu18@gmail.com', + 'datvu19@gmail.com', + 'datvu20@gmail.com', ]; List _values = []; @@ -93,6 +113,7 @@ class _MyHomePageState extends State { delimiters: [',', ' '], hasAddButton: true, resetTextOnSubmitted: true, + suggestionsBoxMaxHeight: 300, // This is set to grey just to illustrate the `textStyle` prop textStyle: const TextStyle(color: Colors.grey), onSubmitted: (outstandingValue) { diff --git a/lib/tag_editor.dart b/lib/tag_editor.dart index 44dda4c..791ddb7 100644 --- a/lib/tag_editor.dart +++ b/lib/tag_editor.dart @@ -354,19 +354,21 @@ class TagsEditorState extends State> { if (renderBox != null) { final size = renderBox!.size; final renderBoxOffset = renderBox!.localToGlobal(Offset.zero); - final topAvailableSpace = renderBoxOffset.dy; + final topAvailableSpace = renderBoxOffset.dy + size.height - 20; final mq = MediaQuery.of(context); final bottomAvailableSpace = mq.size.height - mq.viewInsets.bottom - renderBoxOffset.dy - size.height; - var suggestionBoxHeight = - max(topAvailableSpace, bottomAvailableSpace); - if (null != widget.suggestionsBoxMaxHeight) { - suggestionBoxHeight = - max(suggestionBoxHeight, widget.suggestionsBoxMaxHeight!); - } + debugPrint('TagsEditorState::_createOverlayEntry:topAvailableSpace = $topAvailableSpace | bottomAvailableSpace = $bottomAvailableSpace'); + final maxAvailableSpace = max(topAvailableSpace, bottomAvailableSpace) - 50; + debugPrint('TagsEditorState::_createOverlayEntry:maxAvailableSpace = $maxAvailableSpace | suggestionsBoxMaxHeight = ${widget.suggestionsBoxMaxHeight}'); + final suggestionBoxHeight = widget.suggestionsBoxMaxHeight != null + ? min(maxAvailableSpace, widget.suggestionsBoxMaxHeight!) + : maxAvailableSpace; + debugPrint('TagsEditorState::_createOverlayEntry:suggestionBoxHeight = $suggestionBoxHeight'); final showTop = topAvailableSpace > bottomAvailableSpace; + debugPrint('TagsEditorState::_createOverlayEntry:showTop = $showTop'); return StreamBuilder?>( stream: _suggestionsStreamController?.stream, From 375b9050f2cc161abcad6e7af3dad70465085196 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 15 Jan 2025 03:23:10 +0700 Subject: [PATCH 2/5] Support load more items in suggestion box --- example/lib/main.dart | 24 ++++- lib/tag_editor.dart | 216 ++++++++++++++++++++++++++++-------------- 2 files changed, 170 insertions(+), 70 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 888d5cb..6055412 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -113,7 +113,8 @@ class _MyHomePageState extends State { delimiters: [',', ' '], hasAddButton: true, resetTextOnSubmitted: true, - suggestionsBoxMaxHeight: 300, + suggestionsBoxMaxHeight: 200, + isLoadMoreOnlyOnce: true, // This is set to grey just to illustrate the `textStyle` prop textStyle: const TextStyle(color: Colors.grey), onSubmitted: (outstandingValue) { @@ -200,6 +201,27 @@ class _MyHomePageState extends State { }, suggestionsBoxElevation: 10, findSuggestions: (String query) { + debugPrint( + '_MyHomePageState::build:findSuggestions::query = $query'); + if (query.isNotEmpty) { + var lowercaseQuery = query.toLowerCase(); + return mockResults.sublist(0, 8).where((profile) { + return profile + .toLowerCase() + .contains(query.toLowerCase()) || + profile.toLowerCase().contains(query.toLowerCase()); + }).toList(growable: false) + ..sort((a, b) => a + .toLowerCase() + .indexOf(lowercaseQuery) + .compareTo( + b.toLowerCase().indexOf(lowercaseQuery))); + } + return []; + }, + loadMoreSuggestions: (query) { + debugPrint( + '_MyHomePageState::build:_loadMoreSuggestion::query = $query'); if (query.isNotEmpty) { var lowercaseQuery = query.toLowerCase(); return mockResults.where((profile) { diff --git a/lib/tag_editor.dart b/lib/tag_editor.dart index 791ddb7..77a19cf 100644 --- a/lib/tag_editor.dart +++ b/lib/tag_editor.dart @@ -93,8 +93,10 @@ class TagEditor extends StatefulWidget { this.enableBorder = false, this.autoScrollToInput = true, this.autoHideTextInputField = false, + this.isLoadMoreOnlyOnce = false, this.onFocusTextInput, - this.onSelectOptionAction}) + this.onSelectOptionAction, + this.loadMoreSuggestions}) : assert( !autoHideTextInputField || (!hasAddButton && @@ -197,6 +199,7 @@ class TagEditor extends StatefulWidget { final InputSuggestions findSuggestions; final SearchSuggestions? searchAllSuggestions; final OnSelectOptionAction? onSelectOptionAction; + final InputSuggestions? loadMoreSuggestions; final Color? suggestionsBoxBackgroundColor; final Color? itemHighlightColor; final double? suggestionsBoxRadius; @@ -208,6 +211,7 @@ class TagEditor extends StatefulWidget { final bool useDefaultHighlight; final double? suggestionBoxWidth; final double? suggestionItemHeight; + final bool isLoadMoreOnlyOnce; @override TagsEditorState createState() => TagsEditorState(); @@ -233,10 +237,12 @@ class TagsEditorState extends State> { List? _suggestions; int _searchId = 0; int _countBackspacePressed = 0; + bool _isLoadingMore = false; Debouncer? _deBouncer; final ValueNotifier _highlightedOptionIndex = ValueNotifier(0); final ValueNotifier _validationSuggestionItemNotifier = ValueNotifier(null); + final ValueNotifier _loadingMoreStatus = ValueNotifier(false); RenderBox? get renderBox => context.findRenderObject() as RenderBox?; @@ -269,6 +275,7 @@ class TagsEditorState extends State> { _suggestionsBoxController?.close(); _highlightedOptionIndex.dispose(); _validationSuggestionItemNotifier.dispose(); + _loadingMoreStatus.dispose(); _deBouncer?.cancel(); super.dispose(); } @@ -360,13 +367,17 @@ class TagsEditorState extends State> { mq.viewInsets.bottom - renderBoxOffset.dy - size.height; - debugPrint('TagsEditorState::_createOverlayEntry:topAvailableSpace = $topAvailableSpace | bottomAvailableSpace = $bottomAvailableSpace'); - final maxAvailableSpace = max(topAvailableSpace, bottomAvailableSpace) - 50; - debugPrint('TagsEditorState::_createOverlayEntry:maxAvailableSpace = $maxAvailableSpace | suggestionsBoxMaxHeight = ${widget.suggestionsBoxMaxHeight}'); + debugPrint( + 'TagsEditorState::_createOverlayEntry:topAvailableSpace = $topAvailableSpace | bottomAvailableSpace = $bottomAvailableSpace'); + final maxAvailableSpace = + max(topAvailableSpace, bottomAvailableSpace) - 50; + debugPrint( + 'TagsEditorState::_createOverlayEntry:maxAvailableSpace = $maxAvailableSpace | suggestionsBoxMaxHeight = ${widget.suggestionsBoxMaxHeight}'); final suggestionBoxHeight = widget.suggestionsBoxMaxHeight != null - ? min(maxAvailableSpace, widget.suggestionsBoxMaxHeight!) - : maxAvailableSpace; - debugPrint('TagsEditorState::_createOverlayEntry:suggestionBoxHeight = $suggestionBoxHeight'); + ? min(maxAvailableSpace, widget.suggestionsBoxMaxHeight!) + : maxAvailableSpace; + debugPrint( + 'TagsEditorState::_createOverlayEntry:suggestionBoxHeight = $suggestionBoxHeight'); final showTop = topAvailableSpace > bottomAvailableSpace; debugPrint('TagsEditorState::_createOverlayEntry:showTop = $showTop'); @@ -374,7 +385,76 @@ class TagsEditorState extends State> { stream: _suggestionsStreamController?.stream, initialData: _suggestions, builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { + if (snapshot.data?.isNotEmpty == true) { + Widget listViewWidget = ListView.builder( + shrinkWrap: true, + padding: widget.suggestionPadding ?? EdgeInsets.zero, + itemCount: widget.loadMoreSuggestions != null + ? snapshot.data!.length + 1 + : snapshot.data!.length, + itemBuilder: (context, index) { + if (_suggestions?.isNotEmpty != true) { + return const SizedBox.shrink(); + } + + if (widget.loadMoreSuggestions != null && + index == snapshot.data!.length) { + return ValueListenableBuilder( + valueListenable: _loadingMoreStatus, + builder: (_, value, __) { + debugPrint( + 'TagsEditorState::_createOverlayEntry::ValueListenableBuilder:value = $value'); + if (value) { + return Container( + alignment: Alignment.center, + padding: const EdgeInsets.only(bottom: 12), + child: const SizedBox( + height: 24, + width: 24, + child: CupertinoActivityIndicator(), + ), + ); + } + return const SizedBox.shrink(); + }, + ); + } + + final item = _suggestions![index]; + final highlight = + AutocompleteHighlightedOption.of(context) == index; + final suggestionValid = + ValidationSuggestionItem.of(context); + + final itemWidget = widget.suggestionBuilder( + context, + this, + item, + index, + snapshot.data!.length, + highlight, + suggestionValid, + ); + + if (widget.useDefaultHighlight && highlight) { + return ColoredBox( + color: widget.itemHighlightColor ?? + Theme.of(context).focusColor, + child: itemWidget, + ); + } else { + return itemWidget; + } + }, + ); + + if (widget.loadMoreSuggestions != null) { + listViewWidget = NotificationListener( + onNotification: _onScrollNotification, + child: listViewWidget, + ); + } + final suggestionsListView = TextFieldTapRegion( child: PointerInterceptor( child: AutocompleteHighlightedOption( @@ -385,71 +465,26 @@ class TagsEditorState extends State> { padding: widget.suggestionMargin ?? EdgeInsets.zero, child: Material( elevation: widget.suggestionsBoxElevation ?? 20, - borderRadius: BorderRadius.circular( - widget.suggestionsBoxRadius ?? 20), + borderRadius: BorderRadius.all(Radius.circular( + widget.suggestionsBoxRadius ?? 20)), color: widget.suggestionsBoxBackgroundColor ?? Colors.white, child: ClipRRect( - borderRadius: BorderRadius.circular( - widget.suggestionsBoxRadius ?? 20), + borderRadius: BorderRadius.all(Radius.circular( + widget.suggestionsBoxRadius ?? 20)), child: Container( - decoration: BoxDecoration( - color: widget - .suggestionsBoxBackgroundColor ?? - Colors.white, - borderRadius: BorderRadius.all( - Radius.circular( - widget.suggestionsBoxRadius ?? - 0))), - constraints: BoxConstraints( - maxHeight: suggestionBoxHeight), - child: ListView.builder( - shrinkWrap: true, - padding: widget.suggestionPadding ?? - EdgeInsets.zero, - itemCount: snapshot.data!.length, - itemBuilder: (context, index) { - if (_suggestions != null && - _suggestions?.isNotEmpty == true) { - final item = _suggestions![index]; - final highlight = - AutocompleteHighlightedOption.of( - context) == - index; - final suggestionValid = - ValidationSuggestionItem.of( - context); - - if (!widget.useDefaultHighlight) { - return widget.suggestionBuilder( - context, - this, - item, - index, - snapshot.data!.length, - highlight, - suggestionValid); - } else { - return Container( - color: highlight - ? widget.itemHighlightColor ?? - Theme.of(context) - .focusColor - : null, - child: widget.suggestionBuilder( - context, - this, - item, - index, - snapshot.data!.length, - highlight, - suggestionValid)); - } - } else { - return Container(); - } - }, - )), + decoration: BoxDecoration( + color: widget.suggestionsBoxBackgroundColor ?? + Colors.white, + borderRadius: BorderRadius.all( + Radius.circular( + widget.suggestionsBoxRadius ?? 0)), + ), + constraints: BoxConstraints( + maxHeight: suggestionBoxHeight, + ), + child: listViewWidget, + ), ), ), ), @@ -485,6 +520,46 @@ class TagsEditorState extends State> { ); } + bool _onScrollNotification(ScrollNotification scrollInfo) { + if (scrollInfo is ScrollEndNotification && + scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent && + scrollInfo.metrics.axisDirection == AxisDirection.down && + !_isLoadingMore) { + _loadMoreSuggestion(); + } + return false; + } + + Future _loadMoreSuggestion() async { + debugPrint('TagsEditorState::_loadMoreSuggestion:'); + final currentSuggestionValue = _validationSuggestionItemNotifier.value; + debugPrint( + 'TagsEditorState::_loadMoreSuggestion:currentSuggestionValue = $currentSuggestionValue'); + if (currentSuggestionValue == null) return; + + _isLoadingMore = true; + _loadingMoreStatus.value = true; + + final results = + await widget.loadMoreSuggestions?.call(currentSuggestionValue); + await Future.delayed(const Duration(milliseconds: 4000)); + if (results?.isNotEmpty != true) { + _isLoadingMore = widget.isLoadMoreOnlyOnce; + _loadingMoreStatus.value = false; + return; + } + + if (mounted) { + setState(() => _suggestions = results); + } + _updateHighlight(0); + _suggestionsStreamController?.add(_suggestions ?? []); + _suggestionsBoxController?.open(); + + _isLoadingMore = widget.isLoadMoreOnlyOnce; + _loadingMoreStatus.value = false; + } + void _onTagChanged(String string) { if (string.isNotEmpty) { widget.onTagChanged(string); @@ -538,6 +613,7 @@ class TagsEditorState extends State> { _updateValidationSuggestionItem(value); _suggestionsStreamController?.add(_suggestions ?? []); _suggestionsBoxController?.open(); + _isLoadingMore = false; } void openSuggestionBox() async { @@ -551,6 +627,7 @@ class TagsEditorState extends State> { _updateValidationSuggestionItem(null); _suggestionsStreamController?.add(_suggestions ?? []); _suggestionsBoxController?.open(); + _isLoadingMore = false; } } @@ -562,6 +639,7 @@ class TagsEditorState extends State> { _updateHighlight(0); _updateValidationSuggestionItem(null); _suggestionsBoxController?.close(); + _isLoadingMore = false; } void _scrollToVisible() { From 9db23545fa14fbe7d280f51ab8de39af6e999eca Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 15 Jan 2025 03:28:34 +0700 Subject: [PATCH 3/5] Scroll to top suggestion list when value change --- lib/tag_editor.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/tag_editor.dart b/lib/tag_editor.dart index 77a19cf..21e6da8 100644 --- a/lib/tag_editor.dart +++ b/lib/tag_editor.dart @@ -243,6 +243,7 @@ class TagsEditorState extends State> { final ValueNotifier _validationSuggestionItemNotifier = ValueNotifier(null); final ValueNotifier _loadingMoreStatus = ValueNotifier(false); + final ScrollController _scrollController = ScrollController(); RenderBox? get renderBox => context.findRenderObject() as RenderBox?; @@ -277,6 +278,7 @@ class TagsEditorState extends State> { _validationSuggestionItemNotifier.dispose(); _loadingMoreStatus.dispose(); _deBouncer?.cancel(); + _scrollController.dispose(); super.dispose(); } @@ -388,6 +390,7 @@ class TagsEditorState extends State> { if (snapshot.data?.isNotEmpty == true) { Widget listViewWidget = ListView.builder( shrinkWrap: true, + controller: _scrollController, padding: widget.suggestionPadding ?? EdgeInsets.zero, itemCount: widget.loadMoreSuggestions != null ? snapshot.data!.length + 1 @@ -542,7 +545,6 @@ class TagsEditorState extends State> { final results = await widget.loadMoreSuggestions?.call(currentSuggestionValue); - await Future.delayed(const Duration(milliseconds: 4000)); if (results?.isNotEmpty != true) { _isLoadingMore = widget.isLoadMoreOnlyOnce; _loadingMoreStatus.value = false; @@ -560,6 +562,16 @@ class TagsEditorState extends State> { _loadingMoreStatus.value = false; } + void _scrollToTop() { + if (_scrollController.hasClients) { + _scrollController.animateTo( + 0.0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + } + void _onTagChanged(String string) { if (string.isNotEmpty) { widget.onTagChanged(string); @@ -614,6 +626,7 @@ class TagsEditorState extends State> { _suggestionsStreamController?.add(_suggestions ?? []); _suggestionsBoxController?.open(); _isLoadingMore = false; + _scrollToTop(); } void openSuggestionBox() async { @@ -628,6 +641,7 @@ class TagsEditorState extends State> { _suggestionsStreamController?.add(_suggestions ?? []); _suggestionsBoxController?.open(); _isLoadingMore = false; + _scrollToTop(); } } From 4692906e46866456999208b77a22aa6d833cb594 Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 15 Jan 2025 04:17:49 +0700 Subject: [PATCH 4/5] Keep the order of items in the suggestion list when loading more --- lib/tag_editor.dart | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/lib/tag_editor.dart b/lib/tag_editor.dart index 21e6da8..eca6f60 100644 --- a/lib/tag_editor.dart +++ b/lib/tag_editor.dart @@ -94,6 +94,7 @@ class TagEditor extends StatefulWidget { this.autoScrollToInput = true, this.autoHideTextInputField = false, this.isLoadMoreOnlyOnce = false, + this.isLoadMoreReplaceAllOld = true, this.onFocusTextInput, this.onSelectOptionAction, this.loadMoreSuggestions}) @@ -212,6 +213,7 @@ class TagEditor extends StatefulWidget { final double? suggestionBoxWidth; final double? suggestionItemHeight; final bool isLoadMoreOnlyOnce; + final bool isLoadMoreReplaceAllOld; @override TagsEditorState createState() => TagsEditorState(); @@ -369,19 +371,15 @@ class TagsEditorState extends State> { mq.viewInsets.bottom - renderBoxOffset.dy - size.height; - debugPrint( - 'TagsEditorState::_createOverlayEntry:topAvailableSpace = $topAvailableSpace | bottomAvailableSpace = $bottomAvailableSpace'); + final maxAvailableSpace = max(topAvailableSpace, bottomAvailableSpace) - 50; - debugPrint( - 'TagsEditorState::_createOverlayEntry:maxAvailableSpace = $maxAvailableSpace | suggestionsBoxMaxHeight = ${widget.suggestionsBoxMaxHeight}'); + final suggestionBoxHeight = widget.suggestionsBoxMaxHeight != null ? min(maxAvailableSpace, widget.suggestionsBoxMaxHeight!) : maxAvailableSpace; - debugPrint( - 'TagsEditorState::_createOverlayEntry:suggestionBoxHeight = $suggestionBoxHeight'); + final showTop = topAvailableSpace > bottomAvailableSpace; - debugPrint('TagsEditorState::_createOverlayEntry:showTop = $showTop'); return StreamBuilder?>( stream: _suggestionsStreamController?.stream, @@ -405,8 +403,6 @@ class TagsEditorState extends State> { return ValueListenableBuilder( valueListenable: _loadingMoreStatus, builder: (_, value, __) { - debugPrint( - 'TagsEditorState::_createOverlayEntry::ValueListenableBuilder:value = $value'); if (value) { return Container( alignment: Alignment.center, @@ -534,10 +530,8 @@ class TagsEditorState extends State> { } Future _loadMoreSuggestion() async { - debugPrint('TagsEditorState::_loadMoreSuggestion:'); final currentSuggestionValue = _validationSuggestionItemNotifier.value; - debugPrint( - 'TagsEditorState::_loadMoreSuggestion:currentSuggestionValue = $currentSuggestionValue'); + if (currentSuggestionValue == null) return; _isLoadingMore = true; @@ -551,9 +545,19 @@ class TagsEditorState extends State> { return; } - if (mounted) { - setState(() => _suggestions = results); + if (!widget.isLoadMoreReplaceAllOld && _suggestions?.isNotEmpty == true) { + results!.removeWhere((element) => _suggestions!.contains(element)); + final newList = [..._suggestions!, ...results]; + + if (mounted) { + setState(() => _suggestions = newList); + } + } else { + if (mounted) { + setState(() => _suggestions = results); + } } + _updateHighlight(0); _suggestionsStreamController?.add(_suggestions ?? []); _suggestionsBoxController?.open(); From effd7bd2b43a61796afd737d83a76d8cbdaa4f20 Mon Sep 17 00:00:00 2001 From: dab246 Date: Thu, 16 Jan 2025 12:22:05 +0700 Subject: [PATCH 5/5] Auto load more when user uses arrow down of keyboard --- example/lib/main.dart | 1 + lib/tag_editor.dart | 70 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6055412..1f9b438 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -115,6 +115,7 @@ class _MyHomePageState extends State { resetTextOnSubmitted: true, suggestionsBoxMaxHeight: 200, isLoadMoreOnlyOnce: true, + isLoadMoreReplaceAllOld: false, // This is set to grey just to illustrate the `textStyle` prop textStyle: const TextStyle(color: Colors.grey), onSubmitted: (outstandingValue) { diff --git a/lib/tag_editor.dart b/lib/tag_editor.dart index eca6f60..dbd6826 100644 --- a/lib/tag_editor.dart +++ b/lib/tag_editor.dart @@ -220,6 +220,8 @@ class TagEditor extends StatefulWidget { } class TagsEditorState extends State> { + static const double defaultItemHeight = 50.0; + /// A controller to keep value of the [TextField]. late TextEditingController _textFieldController; late TextDirection _textDirection; @@ -290,11 +292,57 @@ class TagsEditorState extends State> { } void _highlightPreviousOption() { - _updateHighlight(_highlightedOptionIndex.value - 1); + if (_suggestions?.isNotEmpty != true || _highlightedOptionIndex.value <= 0) { + return; + } + + final newIndex = + (_highlightedOptionIndex.value - 1).clamp(0, _suggestions!.length - 1); + _updateHighlight(newIndex); + _scrollToCenterHighlightedItem(); } void _highlightNextOption() { - _updateHighlight(_highlightedOptionIndex.value + 1); + if (_suggestions?.isNotEmpty != true) return; + + if (_highlightedOptionIndex.value >= _suggestions!.length - 1) return; + + final newIndex = + (_highlightedOptionIndex.value + 1).clamp(0, _suggestions!.length - 1); + _updateHighlight(newIndex); + + debugPrint( + 'TagsEditorState::_highlightNextOption:newIndex = $newIndex | _suggestions = ${_suggestions?.length} | _isLoadingMore = $_isLoadingMore'); + + _scrollToCenterHighlightedItem(); + + if (widget.loadMoreSuggestions != null && + _highlightedOptionIndex.value == _suggestions!.length - 1 && + !_isLoadingMore) { + _loadMoreSuggestion(); + } + } + + void _scrollToCenterHighlightedItem() { + debugPrint( + 'TagsEditorState::_scrollToCenterHighlightedItem:_highlightedOptionIndex = ${_highlightedOptionIndex.value}'); + final itemHeight = widget.suggestionItemHeight ?? defaultItemHeight; + final viewportHeight = _scrollController.position.viewportDimension; + final centerOffset = (viewportHeight - itemHeight) / 2; + + double scrollOffset = + (_highlightedOptionIndex.value * itemHeight) - centerOffset; + + scrollOffset = scrollOffset.clamp( + _scrollController.position.minScrollExtent, + _scrollController.position.maxScrollExtent, + ); + + _scrollController.animateTo( + scrollOffset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); } void _selectOption() { @@ -492,12 +540,12 @@ class TagsEditorState extends State> { ), ); - final heightSuggestion = (widget.suggestionItemHeight ?? 50) * - (snapshot.data!.length); + final itemHeight = + widget.suggestionItemHeight ?? defaultItemHeight; + final heightSuggestion = itemHeight * snapshot.data!.length; final offsetY = min(heightSuggestion, suggestionBoxHeight); final compositedTransformFollowerOffset = showTop - ? Offset(0, - -1.0 * (offsetY + (widget.suggestionItemHeight ?? 50))) + ? Offset(0, -1.0 * (offsetY + itemHeight)) : Offset.zero; return Positioned( @@ -531,6 +579,8 @@ class TagsEditorState extends State> { Future _loadMoreSuggestion() async { final currentSuggestionValue = _validationSuggestionItemNotifier.value; + debugPrint( + 'TagsEditorState::_loadMoreSuggestion:currentSuggestionValue = $currentSuggestionValue'); if (currentSuggestionValue == null) return; @@ -539,6 +589,8 @@ class TagsEditorState extends State> { final results = await widget.loadMoreSuggestions?.call(currentSuggestionValue); + debugPrint( + 'TagsEditorState::_loadMoreSuggestion:results = ${results?.length}'); if (results?.isNotEmpty != true) { _isLoadingMore = widget.isLoadMoreOnlyOnce; _loadingMoreStatus.value = false; @@ -546,8 +598,9 @@ class TagsEditorState extends State> { } if (!widget.isLoadMoreReplaceAllOld && _suggestions?.isNotEmpty == true) { - results!.removeWhere((element) => _suggestions!.contains(element)); - final newList = [..._suggestions!, ...results]; + final newSuggestions = + results?.where((element) => !_suggestions!.contains(element)) ?? []; + final newList = [..._suggestions!, ...newSuggestions]; if (mounted) { setState(() => _suggestions = newList); @@ -558,7 +611,6 @@ class TagsEditorState extends State> { } } - _updateHighlight(0); _suggestionsStreamController?.add(_suggestions ?? []); _suggestionsBoxController?.open();