fix(#238): Enter selects the highlighted emoji/mention in autocomplete

The autocomplete only completed an unambiguous emoji on Enter; ambiguous
queries (e.g. `:shrug` with multiple matches) sent the literal text instead of
the highlighted glyph, and mentions only selected via Tab. Enter now accepts
the highlighted item (emoji or mention) like Tab, then sends; Tab still selects
without sending, Esc dismisses to send literal text. Removes the now-dead
_emojiQuery field. (#231, emoji + mentions per Ben)

Widget tests: Enter inserts the highlighted emoji (ambiguous `:shr`) and the
highlighted mention (`@Bo` -> @[Bob]).

Part of #231.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pull/241/head
Strycher 1 week ago
parent d39893f120
commit ad29ea3f0e

@ -42,10 +42,10 @@ class _Entry {
/// - Typing `:` at the start of a word lists matching [emojiShortcodes] and
/// inserts the emoji character. Typing a full `:name:` auto-converts it.
///
/// Up/Down move the highlight, Tab or click selects, Esc dismisses. Enter
/// sends, and first completes an unambiguous emoji (single or exact shortcode
/// match) so it ships the glyph rather than the literal text. Either feature
/// is disabled by leaving its data empty/null.
/// Up/Down move the highlight, Tab or click selects without sending, Esc
/// dismisses. Enter accepts the highlighted item (emoji or mention) then sends;
/// dismiss with Esc first to send literal text. Either feature is disabled by
/// leaving its data empty/null.
class MentionAutocompleteField extends StatefulWidget {
final int maxBytes;
final TextEditingController controller;
@ -84,10 +84,6 @@ class _MentionAutocompleteFieldState extends State<MentionAutocompleteField> {
int _highlighted = -1;
int _tokenStart = -1;
/// Active emoji query while the dropdown shows emoji matches (null for
/// mentions or when closed). Lets Enter complete an unambiguous emoji + send.
String? _emojiQuery;
@override
void initState() {
super.initState();
@ -211,7 +207,6 @@ class _MentionAutocompleteFieldState extends State<MentionAutocompleteField> {
}
void _filterMentions(String query) {
_emojiQuery = null;
if (widget.candidates.isEmpty) {
_close();
return;
@ -306,7 +301,6 @@ class _MentionAutocompleteFieldState extends State<MentionAutocompleteField> {
.toList();
_recentStart = _matches.length; // no divider for emoji
_highlighted = _matches.length - 1;
_emojiQuery = query;
_show();
}
@ -330,7 +324,6 @@ class _MentionAutocompleteFieldState extends State<MentionAutocompleteField> {
_matches = const [];
_highlighted = -1;
_tokenStart = -1;
_emojiQuery = null;
}
}
@ -358,18 +351,14 @@ class _MentionAutocompleteFieldState extends State<MentionAutocompleteField> {
}
if (key == LogicalKeyboardKey.enter ||
key == LogicalKeyboardKey.numpadEnter) {
// For an unambiguous emoji shortcode (a single match, or the exact name),
// Enter completes the glyph first, then sends so `:rofl` + Enter ships
// the emoji, not the literal text. Ambiguous matches still send raw; use
// Tab to pick one deliberately.
final q = _emojiQuery;
if (q != null && _matches.isNotEmpty) {
final best = _matches[_highlighted];
if (_matches.length == 1 || best.label == ':$q:') {
_select(best); // inserts the glyph and closes the dropdown
widget.onSubmitted?.call(widget.controller.text);
return KeyEventResult.handled;
}
// Enter accepts the highlighted item (emoji or mention) like Tab, then
// sends so `:shrug` + Enter ships the highlighted glyph, and `@Bob` +
// Enter inserts the mention. Tab accepts without sending; Esc dismisses
// the dropdown to send literal text. (#231)
if (_highlighted >= 0 && _highlighted < _matches.length) {
_select(_matches[_highlighted]);
widget.onSubmitted?.call(widget.controller.text);
return KeyEventResult.handled;
}
final text = widget.controller.text;
_close();

@ -0,0 +1,90 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/widgets/mention_autocomplete.dart';
void main() {
testWidgets('Enter inserts the highlighted emoji, not literal text (#231)', (
tester,
) async {
final controller = TextEditingController();
final focusNode = FocusNode();
addTearDown(() {
controller.dispose();
focusNode.dispose();
});
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MentionAutocompleteField(
maxBytes: 200,
controller: controller,
focusNode: focusNode,
// Ambiguous query: ':shr' matches both, so the pre-#231 code would
// have sent the literal ':shr' instead of the highlighted glyph.
emojiShortcodes: const {
'shrug': '\u{1F937}',
'shrimp': '\u{1F990}',
},
),
),
),
);
focusNode.requestFocus();
await tester.pump();
controller.value = const TextEditingValue(
text: ':shr',
selection: TextSelection.collapsed(offset: 4),
);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(controller.text.contains(':shr'), isFalse);
expect(
controller.text.contains('\u{1F937}') ||
controller.text.contains('\u{1F990}'),
isTrue,
);
});
testWidgets('Enter inserts the highlighted mention (#231)', (tester) async {
final controller = TextEditingController();
final focusNode = FocusNode();
addTearDown(() {
controller.dispose();
focusNode.dispose();
});
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: MentionAutocompleteField(
maxBytes: 200,
controller: controller,
focusNode: focusNode,
candidates: const [MentionCandidate(name: 'Bob', recent: false)],
),
),
),
);
focusNode.requestFocus();
await tester.pump();
controller.value = const TextEditingValue(
text: '@Bo',
selection: TextSelection.collapsed(offset: 3),
);
await tester.pump();
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
await tester.pump();
expect(controller.text, contains('@[Bob]'));
});
}
Loading…
Cancel
Save

Powered by TurnKey Linux.